Merge branch 'develop' into mountmaster

This commit is contained in:
Blade Barringer 2014-12-31 13:14:11 -06:00
commit 0fd3f290b8
60 changed files with 1244 additions and 456 deletions

View file

@ -21,6 +21,7 @@
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID", "GA_ID": "GA_ID",
"FLAG_REPORT_EMAIL": "email@mod.com",
"EMAIL_SERVER": { "EMAIL_SERVER": {
"url": "http://example.com", "url": "http://example.com",
"authUser": "user", "authUser": "user",
@ -33,13 +34,22 @@
}, },
"PAYPAL":{ "PAYPAL":{
"billing_plans": { "billing_plans": {
"1":"1", "basic_earned":"basic_earned",
"3":"3", "basic_3mo":"basic_3mo",
"6":"6", "basic_6mo":"basic_6mo",
"12":"12" "google_6mo":"google_6mo",
"basic_12mo":"basic_12mo"
}, },
"mode":"sandbox", "mode":"sandbox",
"client_id":"client_id", "client_id":"client_id",
"client_secret":"client_secret" "client_secret":"client_secret"
},
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"LOGGLY": {
"enabled": false,
"subdomain": "subdomain",
"token": "token",
"username": "username",
"password": "password"
} }
} }

1
keys/google/iap-live Normal file
View file

@ -0,0 +1 @@
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk6EVccD7D6y5vLmsUxUXVYTMyByq+NjfTzjMBaKCk93fq8Ahn6Ipha1K4oBIvArUj3CsP5Tb/Yf6c6SrEsSnE3eqgKQgeq+fYyfIm+Fb+tFGTttLonZ2Sqemqeyx2PjOG2CVZBXj1+Zr7jb0ocMNYraYUTvklWnpcK6/5ZaZLLent1PGiU9l5jIfRAsxmJH/UpssgpFPnCs+8PRKUALkJATTfVCCF+PJef6mcKAafAArKUEo0lR2/NeaE62Wpii33DLuKDPfMqL3g4EHZTEVE1ezFl/3WPeLlxuQm38hZLpEfEYKp17qzplU+GmWIfjZt6OdtVdonlYxIxNAZLnWEQIDAQAB

View file

@ -0,0 +1,4 @@
db.users.update({'purchased.plan.consecutive.count':NaN}, {$set:{'purchased.plan.consecutive.count':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.offset':NaN}, {$set:{'purchased.plan.consecutive.offset':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.gemCapExtra':NaN}, {$set:{'purchased.plan.consecutive.gemCapExtra':0}}, {multi:1});
db.users.update({'purchased.plan.consecutive.trinkets':NaN}, {$set:{'purchased.plan.consecutive.trinkets':0}}, {multi:1});

View file

@ -0,0 +1,11 @@
db.users.update(
{'items.gear.owned.head_special_nye':{$ne:null}},
{$set:{'items.gear.owned.head_special_nye2014':false}},
{multi:1}
)
db.users.update(
{'items.gear.owned.head_special_nye':null},
{$set:{'items.gear.owned.head_special_nye':false}},
{multi:1}
)

View file

@ -2,7 +2,7 @@ var _id = '';
var update = { var update = {
$push: { $push: {
'purchased.plan.mysteryItems':{ 'purchased.plan.mysteryItems':{
$each:['head_mystery_201411','weapon_mystery_201411'] $each:['head_mystery_201412','armor_mystery_201412']
} }
} }
}; };

View file

@ -26,9 +26,11 @@
"grunt-nodemon": "~0.3.0", "grunt-nodemon": "~0.3.0",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop", "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"icalendar": "git://github.com/lefnire/node-icalendar#master", "icalendar": "git://github.com/lefnire/node-icalendar#master",
"in-app-purchase": "^0.2.0",
"jade": "~1.7.0", "jade": "~1.7.0",
"js2xmlparser": "~0.1.2", "js2xmlparser": "~0.1.2",
"lodash": "~2.4.1", "lodash": "~2.4.1",
"loggly": "~1.0.8",
"method-override": "~2.2.0", "method-override": "~2.2.0",
"moment": "~2.8.3", "moment": "~2.8.3",
"mongoose": "~3.8.17", "mongoose": "~3.8.17",

View file

@ -30,17 +30,19 @@ future re: pets and whatnot, this is just temporary.
.avatar-level, .avatar-name .avatar-level, .avatar-name
position: absolute position: absolute
@extend $hrpg-label @extend $hrpg-label
z-index: 2
.avatar-level .avatar-level
bottom: 2px bottom: 4px
right: 2px right: 4px
// when a buff/rebirth exists, add space between the icon and the level // when a buff/rebirth exists, add space between the icon and the level
.glyphicon-circle-arrow-up .glyphicon-circle-arrow-up
padding-right: 4px padding-right: 4px
.glyphicon-plus-sign .glyphicon-plus-sign
padding-left: 4px padding-left: 4px
.avatar-name .avatar-name
top: 2px max-width: 132px
left: 2px top: 4px
left: 4px
opacity: 0 // Hide username by default opacity: 0 // Hide username by default
transition: opacity 0.2s ease-out transition: opacity 0.2s ease-out
@ -83,8 +85,9 @@ future re: pets and whatnot, this is just temporary.
width: 50px !important width: 50px !important
.addthis_native_toolbox .addthis_native_toolbox
position: absolute position: absolute
top:2px bottom:4px
right:2px left:4px
z-index: 2
opacity:0 opacity:0
&:hover .addthis_native_toolbox &:hover .addthis_native_toolbox
opacity:1 opacity:1

View file

@ -37,6 +37,9 @@
markdown, span.time markdown, span.time
padding: 0 3px padding: 0 3px
line-height: 1.618em line-height: 1.618em
.long-title .tooltip
white-space: normal
span.time span.time
white-space: nowrap white-space: nowrap

View file

@ -71,7 +71,7 @@ hrpg-button-color-mixin($hrpg-button-color)
color: #fff !important; color: #fff !important;
$hrpg-button-master $hrpg-button-master
list-style: none list-style: none
> a, > button, > input > a, > button, > input, label::after
display: inline-block !important display: inline-block !important
padding: 0.25em 0.5em !important padding: 0.25em 0.5em !important
text-decoration:none text-decoration:none
@ -86,7 +86,7 @@ $hrpg-button-master
background-color: #aaa !important background-color: #aaa !important
$hrpg-button $hrpg-button
@extend $hrpg-button-master @extend $hrpg-button-master
> a, > button, > input > a, > button, > input, label::after
border: 1px solid #ccc !important border: 1px solid #ccc !important
border-radius: 0.382em !important border-radius: 0.382em !important
$hrpg-button-call-to-action $hrpg-button-call-to-action
@ -111,7 +111,7 @@ $hrpg-button-with-input
height: 2em height: 2em
input input
border: 1px solid #ccc border: 1px solid #ccc
border-radius: 0.382em 0em 0em 0.382em border-radius: 0.382em 0em 0em 0.382em !important
padding-left: 0.618em padding-left: 0.618em
background-color: #fff !important background-color: #fff !important
-webkit-appearance: none -webkit-appearance: none
@ -150,15 +150,17 @@ $hrpg-button-bar
border-right: 1px solid darken(#fff,3.82%) border-right: 1px solid darken(#fff,3.82%)
float:left float:left
word-wrap: break-word word-wrap: break-word
button
border: none
li:nth-of-type(2) li:nth-of-type(2)
border-radius: 0.382em 0em 0em 0.382em border-radius: 0.382em 0em 0em 0.382em
li:first-of-type li:first-of-type
border-radius: 0.382em 0em 0em 0.382em border-radius: 0.382em 0em 0em 0.382em
a a, button
border-radius: 0.382em 0em 0em 0.382em border-radius: 0.382em 0em 0em 0.382em
li:last-of-type li:last-of-type
border-right: none border-right: none
a a, button
border-radius: 0em 0.382em 0.382em 0em border-radius: 0em 0.382em 0.382em 0em
$hrpg-button-bar-with-heading $hrpg-button-bar-with-heading

View file

@ -32,4 +32,33 @@
visibility: hidden visibility: hidden
.transparent .transparent
opacity: 0.5 opacity: 0.5
// Allow block element to be centered
.col-centered
margin-left: auto
margin-right: auto
// .pull-right & .pull-left for specific screen sizes
// Follows conventions for col-x-SIZE classes
@media (min-width: 768px)
.pull-left-sm
float: left
.pull-right-sm
float: right
@media (min-width: 992px)
.pull-left-md
float: left
.pull-right-md
float: right
@media (min-width: 1200px)
.pull-left-lg
float: left
.pull-right-lg
float: right

View file

@ -60,6 +60,8 @@ html, body
@media (max-width: 600px) @media (max-width: 600px)
#wrap #wrap
margin-top: 0 margin-top: 0
padding: 0 4px;
overflow-x: hidden;
/* Gems /* Gems
-------------------------------------------------- */ -------------------------------------------------- */
@ -135,6 +137,9 @@ a
a span.glyphicon a span.glyphicon
color: #333 // Takes base color from bootstrap color: #333 // Takes base color from bootstrap
&.text-danger
color: #a94442 // Allows glyphicon to use text-danger class
a.label a.label
color: #fff color: #fff
@ -176,3 +181,5 @@ a.label
.white, .white a .white, .white a
color: #fff !important color: #fff !important
.line-through
text-decoration line-through

View file

@ -4,7 +4,7 @@
line-height: 1.5em line-height: 1.5em
padding-left: 0.75em padding-left: 0.75em
.shop_gold, .shop_silver, .shop_copper .shop_gold, .shop_silver, .shop_copper, .shop_spell
vertical-align: top vertical-align: top
display: inline-block display: inline-block

View file

@ -1,3 +1,7 @@
.quest_collected_true .quest_collected_true
color: #ccc color: #ccc
text-decoration:line-through text-decoration:line-through
quest-rewards
hr
clear:both

View file

@ -48,5 +48,3 @@ a.hint:hover
.tasks-lists .row .col-md-3 .tasks-lists .row .col-md-3
padding-right: 0 padding-right: 0
padding-left: 0 padding-left: 0
.task-column
padding: 0

View file

@ -8,29 +8,99 @@ $stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $goo
// for each color stage, generate a named class w/ the appropriate color // for each color stage, generate a named class w/ the appropriate color
for $stage in $stages for $stage in $stages
.color-{$stage[0]} .task-column:not(.rewards)
background-color: $stage[1] .color-{$stage[0]}:not(.completed)
.action-yesno label,
.task-action-btn
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
.task-options
background-color: $stage[1] background-color: $stage[1]
border: 1px solid shade($stage[1],10%)
.priority-multiplier, .task-attributes, .repeat-days
li
hrpg-button-color-mixin($stage[1])
button
&.active
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.plusminus
.task-checker
label:after
border: 1px solid darken($stage[1], 30%) !important
input[type=checkbox]:checked + label:after
//border: 1px solid darken($stage[1], 50%) !important
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
background-color: darken($stage[1],5%) !important
.save-close, .task-checklist-edit li
hrpg-button-color-mixin(darken($stage[1],5%))
button
&:focus
border: 1px solid darken($stage[1],30%)
outline: 0
.action-yesno label,
.task-action-btn
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
.task-options
background-color: $stage[1]
.option-group:not(.task-checklist)
border-bottom: 1px solid darken($stage[1], 15%)
.option-content
border-color: darken($stage[1], 16.18%) !important
&:hover
border-color: darken($stage[1], 32.8%) !important
&:focus
border-color: darken($stage[1], 61.8%) !important
outline: none;
// completed has to be outside the loop to override the color class // completed has to be outside the loop to override the color class
.completed .completed
color: darken($completed,30%)
background-color: $completed background-color: $completed
color: #999 border: 1px solid shade($completed,10%)
.glyphicon .priority-multiplier, .task-attributes, .repeat-days
color: #333 li
.task-checker label hrpg-button-color-mixin($completed)
background-color: darken($completed, 30%) button
&.active
box-shadow: inset 0 0 0 1px darken($completed,40%) !important
background-color: darken($completed,5%) !important
&:focus
border: 1px solid darken($completed,30%)
outline: 0
.plusminus
.task-checker
label:after
border: 1px solid darken($completed, 30%) !important
input[type=checkbox]:checked + label:after
box-shadow: inset 0 0 0 1px darken($completed,40%) !important
background-color: darken($completed,5%) !important
.save-close, .task-checklist-edit li
hrpg-button-color-mixin(darken($completed,5%))
button
&:focus
border: 1px solid darken($completed,30%)
outline: 0
.action-yesno label,
.task-action-btn .task-action-btn
background-color: darken($completed, 30%) background-color: darken($completed, 30%)
&:hover, &:focus
background-color: darken($completed, 40%)
.task-options
background-color: $completed
.option-group:not(.task-checklist)
border-bottom: 1px solid darken($completed, 15%)
.option-content
border-color: darken($completed, 16.18%) !important
&:hover
border-color: darken($completed, 32.8%) !important
&:focus
border-color: darken($completed, 61.8%) !important
outline: 0;
.reward .reward, .reward-item
background-color: white background-color: white
border: 1px solid $completed !important
// 2. Columns & Tasks // 2. Columns & Tasks
// =================== // ===================
@ -70,7 +140,7 @@ for $stage in $stages
// "Add new task" form // "Add new task" form
// ----------------- // -----------------
.task-add .task-add
margin-top: 1.618em margin-top: 1.5em
@extend $hrpg-button-with-input @extend $hrpg-button-with-input
hrpg-button-color-mixin($color-options-submenu) hrpg-button-color-mixin($color-options-submenu)
input, button input, button
@ -90,7 +160,7 @@ for $stage in $stages
padding: 0 padding: 0
min-height: 3em min-height: 3em
margin-bottom: 0.75em margin-bottom: 0.75em
box-shadow: inset 0 0 1px rgba(0,0,0,0.5) // box-shadow: inset 0 0 1px rgba(0,0,0,0.5)
position: relative position: relative
&:hover &:hover
cursor: move cursor: move
@ -99,6 +169,16 @@ for $stage in $stages
label label
font-weight: 400 font-weight: 400
input[type="text"], input[type="number"], textarea.option-content
border: 1px solid #aaa
border-radius: 0.382em
padding-left: 0.618em
background-color: #fff !important
-webkit-appearance: none
-moz-appearance: none
appearance: none
box-shadow: none //remove red glow in Firefox
// task content // task content
.task-text .task-text
@ -107,7 +187,7 @@ for $stage in $stages
line-height: 1.4 line-height: 1.4
word-wrap: break-word word-wrap: break-word
markdown > ul > li // Otherwise, + gets turned into a bullet and gets maksed by buttons markdown > ul > li // Otherwise, + gets turned into a bullet and gets masked by buttons
margin-left:20px margin-left:20px
span.emoji span.emoji
@ -118,6 +198,8 @@ for $stage in $stages
.habit-wide .task-text .habit-wide .task-text
padding-left: 7em padding-left: 7em
.habit-narrow .task-text
padding-left: 0.75em
// when a task is being dragged // when a task is being dragged
.task.ui-sortable-helper .task.ui-sortable-helper
@ -174,28 +256,21 @@ for $stage in $stages
cursor: pointer cursor: pointer
// plus/minus checkbox // plus/minus checkbox
.action-plusminus .plusminus
label text-align: center
margin-right: 1.5em .task-checker
label:after font-size: 1.5em // 1.41em
font-size: 1.41em @extend $hrpg-button
width: 2.12765em margin-bottom: 0.5em
height: 2.12765em &:first-of-type
line-height: 2.12765 margin-right: 0.5em
text-align: center label:after
display: inline-block width: 2em
vertical-align top line-height: 1.5
opacity: 0.3 label[for$="plus"]:after
background-color: #ddd content: ''
outline: 1px solid rgba(0,0,0,0.5) label[for$="minus"]:after
color: #222 content: ''
label[for$="plus"]:after
content: ''
label[for$="minus"]:after
content: ''
&.select-toggle input[type=checkbox]:checked + label:after
opacity: 1
// yes/no checkbox // yes/no checkbox
.action-yesno .action-yesno
@ -227,9 +302,9 @@ for $stage in $stages
label:hover:after, label:hover:after,
label:focus:after label:focus:after
content: "\E013" content: "\E013"
font-family 'Glyphicons Halflings' font-family: "Glyphicons Halflings"
border none border: none
margin 0 margin: 0
line-height: 1.714285714em line-height: 1.714285714em
height: 1.714285714em height: 1.714285714em
width: 1.714285714em width: 1.714285714em
@ -241,9 +316,9 @@ for $stage in $stages
// completed icon // completed icon
input[type=checkbox]:checked + label:after input[type=checkbox]:checked + label:after
content: "\E013" content: "\E013"
font-family 'Glyphicons Halflings' font-family: "Glyphicons Halflings"
border none border: none
margin 0 margin: 0
line-height: 1.714285714em line-height: 1.714285714em
height: 1.714285714em height: 1.714285714em
width: 1.714285714em width: 1.714285714em
@ -295,36 +370,36 @@ form
//background-color: //background-color:
.option-group .option-group
border-bottom: 1px solid rgba(0,0,0,0.1) //border-bottom: 1px solid rgba(0,0,0,0.1)
padding: 0 0 1em padding: 0 0 1em
margin-bottom: 1em margin-bottom: 1em
.option-title .option-title
font-size: 1em font-size: 1em
margin: 0 0 0.5em margin: 0 0 0.5em
line-height: 1.5 line-height: 2.5
border: 0 border: 0
padding: 0 padding: 0
color: #333 //color: #333
font-style: italic //font-style: italic
&:not(.mega):after font-weight: bold
content: ':' text-align: center
&.mega &.mega
cursor: pointer cursor: pointer
font-style: normal //font-style: normal
font-weight: bold &:after
text-decoration: underline font-family: "Glyphicons Halflings"
font-size: 0.75em
content: "\E114"
padding-left: 0.75em
//&:not(.mega):after
//content: ':'
.option-content .option-content
height: 2.5em height: 2.5em
width: 100% width: 100%
margin: 0 0 1em margin: 0 0 1em
padding: 0 0 0 0.5em padding: 0 0 0 0.5em
outline: 0
border: 1px solid rgba(0,0,0,0.2)
box-shadow: none
border-radius: 0
font-family: 'Lato', sans-serif //bootstrap override
textarea.option-content textarea.option-content
height: 5em height: 5em
@ -393,47 +468,66 @@ form
.task-options .task-options
.task-action-btn.tile .task-action-btn.tile
margin: 3px margin: 3px
form
padding-bottom: 1em
.priority-multiplier, .task-attributes, .repeat-days
text-align: center
li
@extend $hrpg-button
margin-bottom: 1em
margin-right: 0.75em
display: inline-block
&:last-of-type
margin-right: 0
.repeat-days
li
button
min-width: 2.5em
.save-close
text-align: center
@extend $hrpg-button
// todos ui // todos ui
// -------- // --------
// Checklists // Checklists
// -------- // --------
.checklist-form .task-checklist-edit
/* Make an input blend into its parent */ > ul
input.inline-edit text-align: center
reset-box-model() li
reset-font() @extend $hrpg-button
line-height: inherit button
text-align: inherit span
background-color:inherit margin-right: 0.5em
box-shadow: none > form
width: 80% li
@extend $hrpg-button-with-input
@extend $clearfix
input
width: 70%
/* Add interaction cues on hover and focus */ /* Add interaction cues on hover and focus */
input.inline-edit:hover,input.inline-edit:focus &:hover,&:focus
background-color: #FFD background-color: #FFD
transition: background-color 0.5s transition: background-color 0.5s
.checklist-icons .checklist-icon
opacity:0 opacity:0.25
width: 5% text-align: center
line-height: 1.5
li float: left
min-height: 20px padding: 0.25em 0.5em
padding-left: 10px
margin-bottom: 5px li
margin-bottom: 1em
[type="checkbox"] &:hover .checklist-icon
margin: 0px 10px 3px 0px opacity:1
width: 4%
li:hover .checklist-icons
opacity:1
// Task filters // Task filters
// -------- // --------
.task-filter .task-filter
margin: 1.618em 0 1em 0 margin: 1.5em 0 1em 0
hrpg-button-bar-mixin($color-tasks) hrpg-button-bar-mixin($color-tasks)
@extend $hrpg-button-bar @extend $hrpg-button-bar
width: 100% width: 100%
@ -449,16 +543,18 @@ form
width: 100% width: 100%
height: 100% height: 100%
.rewards .rewards
.task
min-height: 3.07em // Temp fix for slight misalignment of btn-buy
.task-filter .task-filter
li:nth-child(3n + 1):nth-last-child(2) li:nth-child(3n + 1):nth-last-child(2)
width: 33.333% width: 33.333%
li:nth-child(3n + 1):nth-last-child(2) + li li:nth-child(3n + 1):nth-last-child(2) + li
width: 66.666% width: 66.666%
.btn-buy .btn-buy
word-wrap: break-word word-wrap: break-word
padding-left: 0.2em padding-left: 0.2em
text-align: left text-align: left
border:none
span span
position: relative position: relative
left: -2px left: -2px
@ -474,9 +570,4 @@ form
margin: auto margin: auto
.shop_gold + .reward-cost .shop_gold + .reward-cost
line-height: 1 line-height: 1
top: -0.1em top: -0.1em
.repeat-days
a
border-radius: 0.382em

View file

@ -7,7 +7,6 @@ window.habitrpg = angular.module('habitrpg',
// @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705 // @see https://github.com/angular-ui/ui-router/issues/110 and https://github.com/HabitRPG/habitrpg/issues/1705
// temporary hack until they have a better solution // temporary hack until they have a better solution
.value('$anchorScroll', angular.noop)
.constant("API_URL", "") .constant("API_URL", "")
.constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_USER_ID", 'habitrpg-user')
@ -185,6 +184,10 @@ window.habitrpg = angular.module('habitrpg',
url: '/timetravelers', url: '/timetravelers',
templateUrl: "partials/options.inventory.timetravelers.html" templateUrl: "partials/options.inventory.timetravelers.html"
}) })
.state('options.inventory.seasonalshop', {
url: '/seasonalshop',
templateUrl: "partials/options.inventory.seasonalshop.html"
})
// Options > Settings // Options > Settings
.state('options.settings', { .state('options.settings', {

View file

@ -42,6 +42,7 @@
//$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in <head> //$.getScript("//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4"); //FIXME why isn't this working when here? instead it's now in <head>
var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://',''); var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://','');
window.addthis_config = { window.addthis_config = {
ui_click: true,
services_custom:{ services_custom:{
name: "Download", name: "Download",
url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png", url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png",

View file

@ -108,8 +108,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
} }
]) ])
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups',
function($scope, $rootScope, Members, Shared, $http, Notification) { function($scope, $rootScope, Members, Shared, $http, Notification, Groups) {
$scope.timestamp = function(timestamp){ $scope.timestamp = function(timestamp){
return moment(timestamp).format('MM/DD/YYYY'); return moment(timestamp).format('MM/DD/YYYY');
} }
@ -129,7 +129,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.gift = { $scope.gift = {
type: 'gems', type: 'gems',
gems: {amount:0, fromBalance:true}, gems: {amount:0, fromBalance:true},
subscription: {months:0}, subscription: {key:''},
message:'' message:''
}; };
$scope.sendGift = function(uuid, gift){ $scope.sendGift = function(uuid, gift){
@ -139,6 +139,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.$close(); $scope.$close();
}) })
} }
$scope.reportAbuse = function(reporter, message, groupId) {
message.flags[reporter._id] = true;
Groups.Group.flagChatMessage({gid: groupId, messageId: message.id}, undefined, function(data){
Notification.text(window.env.t('abuseReported'));
$scope.$close();
});
}
$scope.clearFlagCount = function(message, groupId) {
Groups.Group.clearFlagCount({gid: groupId, messageId: message.id}, undefined, function(data){
message.flagCount = 0;
Notification.text("Flags cleared");
$scope.$close();
});
}
} }
]) ])
@ -197,7 +211,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}); });
}]) }])
.controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', function($scope, Groups, User, $http, ApiUrlService, Notification){ .controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', 'Members', '$rootScope', function($scope, Groups, User, $http, ApiUrlService, Notification, Members, $rootScope){
$scope.message = {content:''}; $scope.message = {content:''};
$scope._sending = false; $scope._sending = false;
@ -239,12 +253,14 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.deleteChatMessage = function(group, message){ $scope.deleteChatMessage = function(group, message){
if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){ if(message.uuid === User.user.id || (User.user.backer && User.user.contributor.admin)){
var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false; var previousMsg = (group.chat && group.chat[0]) ? group.chat[0].id : false;
Groups.Group.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){ if(confirm('Are you sure you want to delete this message?')){
if(data.chat) group.chat = data.chat; Groups.Group.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
var i = _.findIndex(group.chat, {id: message.id}); var i = _.findIndex(group.chat, {id: message.id});
if(i !== -1) group.chat.splice(i, 1); if(i !== -1) group.chat.splice(i, 1);
}); });
}
} }
} }
@ -262,6 +278,22 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$http.post(ApiUrlService.get() + '/api/v2/groups/' + group._id + '/chat/' + message.id + '/like'); $http.post(ApiUrlService.get() + '/api/v2/groups/' + group._id + '/chat/' + message.id + '/like');
} }
$scope.flagChatMessage = function(groupId,message) {
if(!message.flags) message.flags = {};
if(message.flags[User.user._id])
Notification.text(window.env.t('abuseAlreadyReported'));
else {
$scope.abuseObject = message;
$scope.groupId = groupId;
Members.selectMember(message.uuid, function(){
$rootScope.openModal('abuse-flag',{
controller:'MemberModalCtrl',
scope: $scope
});
});
}
}
$scope.sync = function(group){ $scope.sync = function(group){
group.$get(); group.$get();
} }

View file

@ -88,16 +88,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
} }
$scope.purchase = function(type, item){ $scope.purchase = function(type, item){
if (item.key=='spookDust') return User.user.ops.buySpookDust({}); if (type == 'special') return User.user.ops.buySpecialSpell({params:{key:item.key}});
var gems = User.user.balance * 4; var gems = User.user.balance * 4;
if(gems < item.value) return $rootScope.openModal('buyGems'); var string = (type == 'weapon') ? window.env.t('weapon') : (type == 'armor') ? window.env.t('armor') : (type == 'head') ? window.env.t('headgear') : (type == 'shield') ? window.env.t('offhand') : (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : type; // this is ugly but temporary, once the purchase modal is done this will be removed
var string = (type == 'hatchingPotions') ? window.env.t('hatchingPotion') : (type == 'eggs') ? window.env.t('eggSingular') : (type == 'quests') ? window.env.t('quest') : (item.key == 'Saddle') ? window.env.t('foodSaddleText').toLowerCase() : (type == 'special') ? item.key : type; // this is ugly but temporary, once the purchase modal is done this will be removed if (type == 'weapon' || type == 'armor' || type == 'head' || type == 'shield') {
var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems}) if (gems < ((item.specialClass == "wizard") && (item.type == "weapon")) + 1) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: ((item.specialClass == "wizard") && (item.type == "weapon")) + 1, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:"gear",key:item.key}});
} else {
if(gems < item.value) return $rootScope.openModal('buyGems');
var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
if($window.confirm(message))
User.user.ops.purchase({params:{type:type,key:item.key}});
}
if($window.confirm(message))
User.user.ops.purchase({params:{type:type,key:item.key}});
} }
$scope.choosePet = function(egg, potion){ $scope.choosePet = function(egg, potion){
@ -157,5 +164,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
$rootScope.selectedQuest = item; $rootScope.selectedQuest = item;
$rootScope.openModal('buyQuest', {controller:'InventoryCtrl'}); $rootScope.openModal('buyQuest', {controller:'InventoryCtrl'});
} }
$scope.getSeasonalShopArray = function(set){
var flatGearArray = _.toArray(Content.gear.flat);
var filteredArray = _.where(flatGearArray, {index: set});
return filteredArray;
};
$scope.getSeasonalShopQuests = function(set){
var questArray = _.toArray(Content.quests);
var filteredArray = _.filter(questArray, function(q){
return q.key == "evilsanta" || q.key == "evilsanta2";
});
return filteredArray;
};
} }
]); ]);

View file

@ -195,6 +195,9 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$scope.castStart = function(spell) { $scope.castStart = function(spell) {
if (User.user.stats.mp < spell.mana) return Notification.text(window.env.t('notEnoughMana')); if (User.user.stats.mp < spell.mana) return Notification.text(window.env.t('notEnoughMana'));
if (spell.key == 'nye' && User.user.stats.gp < spell.value)
return Notification.text('Not enough gold.');
$rootScope.applyingAction = true; $rootScope.applyingAction = true;
$scope.spell = spell; $scope.spell = spell;
if (spell.target == 'self') { if (spell.target == 'self') {
@ -236,7 +239,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$scope.spell = null; $scope.spell = null;
} }
// Becuase our angular-ui-router uses anchors for urls (/#/options/groups/party), window.location.href=... won't // Because our angular-ui-router uses anchors for urls (/#/options/groups/party), window.location.href=... won't
// reload the page. Perform manually. // reload the page. Perform manually.
$rootScope.hardRedirect = function(url){ $rootScope.hardRedirect = function(url){
window.location.href = url; window.location.href = url;

View file

@ -65,6 +65,8 @@ habitrpg.controller('SettingsCtrl',
User.set({'preferences.language': $scope.language.code}); User.set({'preferences.language': $scope.language.code});
} }
$scope.availableFormats = ['MM/dd/yyyy','dd/MM/yyyy', 'yyyy/MM/dd'];
$scope.reroll = function(){ $scope.reroll = function(){
User.user.ops.reroll({}); User.user.ops.reroll({});
$rootScope.$state.go('tasks'); $rootScope.$state.go('tasks');
@ -179,5 +181,15 @@ habitrpg.controller('SettingsCtrl',
$scope.deleteWebhook = function(id) { $scope.deleteWebhook = function(id) {
User.user.ops.deleteWebhook({params:{id:id}}); User.user.ops.deleteWebhook({params:{id:id}});
} }
$scope.applyCoupon = function(coupon){
$http.get(ApiUrlService.get() + '/api/v2/coupons/valid-discount/'+coupon)
.success(function(){
Notification.text("Coupon applied!");
var subs = $scope.Content.subscriptionBlocks;
subs["basic_6mo"].discount = true;
subs["google_6mo"].discount = false;
});
}
} }
]); ]);

View file

@ -108,7 +108,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
*/ */
function focusChecklist(task,index) { function focusChecklist(task,index) {
window.setTimeout(function(){ window.setTimeout(function(){
$('#task-'+task.id+' .checklist-form .inline-edit')[index].focus(); $('#task-'+task.id+' .checklist-form input[type="text"]')[index].focus();
}); });
} }
$scope.addChecklist = function(task) { $scope.addChecklist = function(task) {

View file

@ -13,6 +13,8 @@ angular.module('groupServices', ['ngResource']).
//query: {method: "GET", isArray:false}, //query: {method: "GET", isArray:false},
postChat: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat'}, postChat: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat'},
deleteChatMessage: {method: "DELETE", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId'}, deleteChatMessage: {method: "DELETE", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId'},
flagChatMessage: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/flag'},
clearFlagCount: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/clearflags'},
join: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/join'}, join: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/join'},
leave: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/leave'}, leave: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/invite'}, invite: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/invite'},

View file

@ -3,23 +3,12 @@
angular.module('paymentServices',[]).factory('Payments', angular.module('paymentServices',[]).factory('Payments',
['$rootScope', 'User', '$http', 'Content', ['$rootScope', 'User', '$http', 'Content',
function($rootScope, User, $http, Content) { function($rootScope, User, $http, Content) {
var user = User.user;
var plan = User.user.purchased.plan;
var Payments = {}; var Payments = {};
Payments.currentSub = _.find(Content.subscriptionBlocks, function(b){
switch (plan.paymentMethod) {
case 'Stripe':
case 'Paypal': // FIXME store paypalKey somewhere?
return b.key == plan.planId;
default: return undefined;
}
})
Payments.showStripe = function(data) { Payments.showStripe = function(data) {
var sub = var sub =
data.subscription ? data.subscription data.subscription ? data.subscription
: data.gift && data.gift.type=='subscription' ? data.gift.subscription.months : data.gift && data.gift.type=='subscription' ? data.gift.subscription.key
: false; : false;
sub = sub && Content.subscriptionBlocks[sub]; sub = sub && Content.subscriptionBlocks[sub];
var amount = // 500 = $5 var amount = // 500 = $5
@ -36,7 +25,8 @@ function($rootScope, User, $http, Content) {
token: function(res) { token: function(res) {
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift); if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
if (data.subscription) url += '&sub='+sub.months; if (data.subscription) url += '&sub='+sub.key;
if (data.coupon) url += '&coupon='+data.coupon;
$http.post(url, res).success(function() { $http.post(url, res).success(function() {
window.location.reload(true); window.location.reload(true);
}).error(function(res) { }).error(function(res) {
@ -50,9 +40,9 @@ function($rootScope, User, $http, Content) {
StripeCheckout.open({ StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY, key: window.env.STRIPE_PUB_KEY,
address: false, address: false,
name: 'Update', name: window.env.t('subUpdateTitle'),
description: 'Update the card to be charged for your subscription', description: window.env.t('subUpdateDescription'),
panelLabel: 'Update Card', panelLabel: window.env.t('subUpdateCard'),
token: function(data) { token: function(data) {
var url = '/stripe/subscribe/edit'; var url = '/stripe/subscribe/edit';
$http.post(url, data).success(function() { $http.post(url, data).success(function() {
@ -66,7 +56,7 @@ function($rootScope, User, $http, Content) {
Payments.cancelSubscription = function(){ Payments.cancelSubscription = function(){
if (!confirm(window.env.t('sureCancelSub'))) return; if (!confirm(window.env.t('sureCancelSub'))) return;
window.location.href = '/' + plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + user._id + '&apiToken=' + user.apiToken; window.location.href = '/' + User.user.purchased.plan.paymentMethod.toLowerCase() + '/subscribe/cancel?_id=' + User.user._id + '&apiToken=' + User.user.apiToken;
} }
Payments.encodeGift = function(uuid, gift){ Payments.encodeGift = function(uuid, gift){

View file

@ -148,21 +148,19 @@ api.loginSocial = function(req, res, next) {
network = req.body.network; network = req.body.network;
if (network!=='facebook') if (network!=='facebook')
return res.json(401, {err:"Only Facebook supported currently."}); return res.json(401, {err:"Only Facebook supported currently."});
async.waterfall([ async.auto({
function(cb){ profile: function (cb) {
passport._strategies[network].userProfile(access_token, cb); passport._strategies[network].userProfile(access_token, cb);
}, },
function(profile, cb) { user: ['profile', function (cb, results) {
var q = {};q['auth.'+network+'.id'] = profile.id; var q = {};
User.findOne(q, {_id:1, apiToken:1, auth:1}, function(err, user){ q['auth.' + network + '.id'] = results.profile.id;
if (err) return cb(err); User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
cb(null, {user:user, profile:profile}); }],
}); register: ['profile', 'user', function (cb, results) {
}, if (results.user) return cb(null, results.user);
function(data, cb){
if (data.user) return cb(null, data.user);
// Create new user // Create new user
var prof = data.profile; var prof = results.profile;
var user = { var user = {
preferences: { preferences: {
language: req.language // User language detected from browser, not saved language: req.language // User language detected from browser, not saved
@ -175,15 +173,16 @@ api.loginSocial = function(req, res, next) {
user = new User(user); user = new User(user);
user.save(cb); user.save(cb);
if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){ if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
utils.txnEmail({name:prof.displayName || prof.username, email:prof.emails[0].value}, 'welcome'); utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
} }
ga.event('register', network).send(); ga.event('register', network).send();
} }]
], function(err, user){ }, function(err, results){
if (err) return res.json(401, {err: err.toString ? err.toString() : err}); if (err) return res.json(401, {err: err.toString ? err.toString() : err});
if (user.auth.blocked) return res.json(401, accountSuspended(user._id)); var acct = results.register[0] ? results.register[0] : results.register;
return res.json(200, {id: user.id, token:user.apiToken}); if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
return res.json(200, {id:acct._id, token:acct.apiToken});
}) })
}; };
@ -206,16 +205,15 @@ api.resetPassword = function(req, res, next){
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email}); if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
user.auth.local.salt = salt; user.auth.local.salt = salt;
user.auth.local.hashed_password = hashed_password; user.auth.local.hashed_password = hashed_password;
utils.sendEmail({ utils.txnEmail(user, 'reset-password', [
from: "HabitRPG <admin@habitrpg.com>", {name: "NEW_PASSWORD", content: newPassword},
to: email, {name: "USERNAME", content: user.auth.local.username}
subject: "Password Reset for HabitRPG", ]);
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL'), user.save(function(err){
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong>. Log in at " + nconf.get('BASE_URL') if(err) return next(err);
res.send('New password sent to '+ email);
email = salt = newPassword = hashed_password = null;
}); });
user.save();
res.send('New password sent to '+ email);
email = salt = newPassword = hashed_password = null;
}); });
}; };

View file

@ -147,69 +147,63 @@ api.getMember = function(req, res, next) {
// CREATE // CREATE
api.create = function(req, res, next){ api.create = function(req, res, next){
var user = res.locals.user; var user = res.locals.user;
var group, chal;
// First, make sure they've selected a legit group, and store it for later async.auto({
var waterfall = [ get_group: function(cb){
function(cb){ var q = {_id:req.body.group};
Group.findById(req.body.group).exec(cb); if (req.body.group!='habitrpg') q.members = {$in:[user._id]}; // make sure they're a member of the group
Group.findOne(q, cb);
}, },
function(_group, cb){ save_chal: ['get_group', function(cb, results){
if (!_group) return cb("Group." + req.body.group + " not found"); var group = results.get_group,
group = _group; prize = +req.body.prize;
cb(null); if (!group)
} return cb({code:404, err:"Group." + req.body.group + " not found"});
]; if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id)
return cb({code:401, err: "Only the group leader can create challenges"});
// If they're adding a prize, do some validation
if (prize < 0)
return cb({code:401, err: 'Challenge prize must be >= 0'});
if (req.body.group=='habitrpg' && prize < 1)
return cb({code:401, err: 'Prize must be at least 1 Gem for public challenges.'});
if (prize > 0) {
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
var prizeCost = prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
if (prizeCost > user.balance + groupBalance)
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
// If they're adding a prize, do some validation if (groupBalance >= prizeCost) {
if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'}); // Group pays for all of prize
if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'}); group.balance -= prizeCost;
if (+req.body.prize > 0) { } else if (groupBalance > 0) {
waterfall.push(function(cb){ // User pays remainder of prize cost after group
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0); var remainder = prizeCost - group.balance;
var prizeCost = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid... group.balance = 0;
if (prizeCost > user.balance + groupBalance) user.balance -= remainder;
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.") } else {
// User pays for all of prize
if (groupBalance >= prizeCost) { user.balance -= prizeCost;
// Group pays for all of prize }
group.balance -= prizeCost; }
} else if (groupBalance > 0) {
// User pays remainder of prize cost after group
var remainder = prizeCost - group.balance;
group.balance = 0;
user.balance -= remainder;
} else {
// User pays for all of prize
user.balance -= prizeCost;
}
cb(null);
});
}
waterfall = waterfall.concat([
function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb`
req.body.leader = user._id; req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official; req.body.official = user.contributor.admin && req.body.official;
var chal = new Challenge(req.body); // FIXME sanitize var chal = new Challenge(req.body); // FIXME sanitize
chal.members.push(user._id); chal.members.push(user._id);
chal.save(cb) chal.save(cb);
}, }],
function(_chal, num, cb){ save_group: ['save_chal', function(cb, results){
chal = _chal; results.get_group.challenges.push(results.save_chal[0]._id);
group.challenges.push(chal._id); results.get_group.save(cb);
group.save(cb); }],
}, sync_user: ['save_group', function(cb, results){
function(_group, num, cb) {
// Auto-join creator to challenge (see members.push above) // Auto-join creator to challenge (see members.push above)
chal.syncToUser(user, cb); results.save_chal[0].syncToUser(user, cb);
} }]
]); }, function(err, results){
async.waterfall(waterfall, function(err){ if (err) return err.code? res.json(err.code, err) : next(err);
if (err) return next(err); return res.json(results.save_chal[0]);
res.json(chal); user = null;
user = group = chal = null; })
});
} }
// UPDATE // UPDATE

View file

@ -7,10 +7,12 @@ function clone(a) {
var _ = require('lodash'); var _ = require('lodash');
var nconf = require('nconf'); var nconf = require('nconf');
var async = require('async'); var async = require('async');
var utils = require('./../utils');
var shared = require('habitrpg-shared'); var shared = require('habitrpg-shared');
var User = require('./../models/user').model; var User = require('./../models/user').model;
var Group = require('./../models/group').model; var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model; var Challenge = require('./../models/challenge').model;
var isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports; var api = module.exports;
/* /*
@ -187,7 +189,7 @@ api.update = function(req, res, next) {
if(group.leader !== user._id) if(group.leader !== user._id)
return res.json(401, {err: "Only the group leader can update the group!"}); return res.json(401, {err: "Only the group leader can update the group!"});
'name description logo logo leaderMessage leader'.split(' ').forEach(function(attr){ 'name description logo logo leaderMessage leader leaderOnly'.split(' ').forEach(function(attr){
group[attr] = req.body[attr]; group[attr] = req.body[attr];
}); });
@ -256,6 +258,82 @@ api.deleteChatMessage = function(req, res, next){
}); });
} }
api.flagChatMessage = function(req, res, next){
var user = res.locals.user
var group = res.locals.group;
var message = _.find(group.chat, {id: req.params.mid});
if(!message) return res.json(404, {err: "Message not found!"});
if(message.uuid == user._id) return res.json(401, {err: "Can't report your own message."});
User.findOne({_id: message.uuid}, {auth: 1}, function(err, author){
if(err) return next(err);
// Log user ids that have flagged the message
if(!message.flags) message.flags = {};
if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: "You have already reported this message"});
message.flags[user._id] = true;
// Log total number of flags (publicly viewable)
if(!message.flagCount) message.flagCount = 0;
if(user.contributor.admin){
// Arbitraty amount, higher than 2
message.flagCount = 5;
} else {
message.flagCount++
}
group.markModified('chat');
group.save(function(err,_saved){
if(err) return next(err);
if (isProd){
utils.txnEmail({email: nconf.get('FLAG_REPORT_EMAIL')}, 'flag-report-to-mods', [
{name: "MESSAGE_TIME", content: (new Date(message.timestamp)).toString()},
{name: "MESSAGE_TEXT", content: message.text},
{name: "REPORTER_USERNAME", content: user.profile.name},
{name: "REPORTER_UUID", content: user._id},
{name: "REPORTER_EMAIL", content: user.auth.local ? user.auth.local.email : ((user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0]) ? user.auth.facebook.emails[0].value : null)},
{name: "REPORTER_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + user._id},
{name: "AUTHOR_USERNAME", content: message.user},
{name: "AUTHOR_UUID", content: message.uuid},
{name: "AUTHOR_EMAIL", content: author.auth.local ? author.auth.local.email : ((author.auth.facebook && author.auth.facebook.emails && author.auth.facebook.emails[0]) ? author.auth.facebook.emails[0].value : null)},
{name: "AUTHOR_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + message.uuid},
{name: "GROUP_NAME", content: group.name},
{name: "GROUP_TYPE", content: group.type},
{name: "GROUP_ID", content: group._id},
{name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')},
]);
}
return res.send(204);
});
});
}
api.clearFlagCount = function(req, res, next){
var user = res.locals.user
var group = res.locals.group;
var message = _.find(group.chat, {id: req.params.mid});
if(!message) return res.json(404, {err: "Message not found!"});
if(user.contributor.admin){
message.flagCount = 0;
group.markModified('chat');
group.save(function(err,_saved){
if(err) return next(err);
return res.send(204);
});
}else{
return res.json(401, {err: "Only an admin can clear the flag count!"})
}
}
api.seenMessage = function(req,res,next){ api.seenMessage = function(req,res,next){
// Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save // Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save
// Check for req.params.gid to exist // Check for req.params.gid to exist

View file

@ -35,7 +35,7 @@ api.sendMessage = function(user, member, data){
msg = data.message msg = data.message
} else { } else {
msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you "; msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you ";
msg += (data.type=='gems') ? data.gems.amount + " gems!`" : data.subscription.months + " months of subscription!`"; msg += (data.type=='gems') ? data.gems.amount + " gems!`" : shared.content.subscriptionBlocks[data.subscription.key].months + " months of subscription!`";
msg += data.message; msg += data.message;
} }
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user)); shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));

View file

@ -0,0 +1,129 @@
var iap = require('in-app-purchase');
var async = require('async');
var payments = require('./index');
var nconf = require('nconf');
var inAppPurchase = require('in-app-purchase');
inAppPurchase.config({
// this is the path to the directory containing iap-sanbox/iap-live files
googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR")
});
// Validation ERROR Codes
var INVALID_PAYLOAD = 6778001;
var CONNECTION_FAILED = 6778002;
var PURCHASE_EXPIRED = 6778003;
exports.androidVerify = function(req, res, next) {
var iapBody = req.body;
var user = res.locals.user;
iap.setup(function (error) {
if (error) {
var resObj = {
ok: false,
data: 'IAP Error'
};
console.error('IAP Setup ERROR');
console.error(error);
res.json(resObj);
return;
}
/*
google receipt must be provided as an object
{
"data": "{stringified data object}",
"signature": "signature from google"
}
*/
var testObj = {
data: iapBody.transaction.receipt,
signature: iapBody.transaction.signature
};
// iap is ready
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
if (err) {
var resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
};
res.json(resObj);
console.error(err);
return;
}
if (iap.isValidated(googleRes)) {
var resObj = {
ok: true,
data: googleRes
};
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'});
// yay good!
res.json(resObj);
}
});
});
};
exports.iosVerify = function(req, res, next) {
console.info(req.body);
var iapBody = req.body;
var user = res.locals.user;
iap.setup(function (error) {
if (error) {
var resObj = {
ok: false,
data: 'IAP Error'
};
console.error('IAP Setup ERROR');
console.error(error);
res.json(resObj);
return;
}
// iap is ready
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
if (err) {
var resObj = {
ok: false,
data: {
code: INVALID_PAYLOAD,
message: err.toString()
}
};
res.json(resObj);
console.error(err);
return;
}
if (iap.isValidated(appleRes)) {
var resObj = {
ok: true,
data: appleRes
};
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
// yay good!
res.json(resObj);
}
});
});
};

View file

@ -7,9 +7,11 @@ var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production"; var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe'); var stripe = require('./stripe');
var paypal = require('./paypal'); var paypal = require('./paypal');
var User = require('mongoose').model('User');
var members = require('../members') var members = require('../members')
var async = require('async'); var async = require('async');
var iap = require('./iap');
var mongoose= require('mongoose');
var cc = require('coupon-code');
function revealMysteryItems(user) { function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) { _.each(shared.content.gear.flat, function(item) {
@ -29,8 +31,8 @@ exports.createSubscription = function(data, cb) {
var recipient = data.gift ? data.gift.member : data.user; var recipient = data.gift ? data.gift.member : data.user;
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case //if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case
var p = recipient.purchased.plan; var p = recipient.purchased.plan;
var months = +(data.gift ? data.gift.subscription.months : data.sub.months); var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
var block = shared.content.subscriptionBlocks[months]; var months = +block.months;
if (data.gift) { if (data.gift) {
if (p.customerId && !p.dateTerminated) { // User has active plan if (p.customerId && !p.dateTerminated) { // User has active plan
@ -114,6 +116,14 @@ exports.buyGems = function(data, cb) {
], cb); ], cb);
} }
exports.validCoupon = function(req, res, next){
mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
if (err) return next(err);
if (!coupon) return res.json(401, {err:"Invalid coupon code"});
return res.send(200);
});
}
exports.stripeCheckout = stripe.checkout; exports.stripeCheckout = stripe.checkout;
exports.stripeSubscribeCancel = stripe.subscribeCancel; exports.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit; exports.stripeSubscribeEdit = stripe.subscribeEdit;
@ -123,4 +133,7 @@ exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription; exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment; exports.paypalCheckout = paypal.createPayment;
exports.paypalCheckoutSuccess = paypal.executePayment; exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn; exports.paypalIPN = paypal.ipn;
exports.iapAndroidVerify = iap.androidVerify;
exports.iapIosVerify = iap.iosVerify;

View file

@ -9,12 +9,14 @@ var logger = require('../../logging');
var ipn = require('paypal-ipn'); var ipn = require('paypal-ipn');
var paypal = require('paypal-rest-sdk'); var paypal = require('paypal-rest-sdk');
var shared = require('habitrpg-shared'); var shared = require('habitrpg-shared');
var mongoose = require('mongoose');
var cc = require('coupon-code');
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have // This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created // a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
// there, get it's plan.id and store it in config.json // there, get it's plan.id and store it in config.json
_.each(shared.content.subscriptionBlocks, function(block){ _.each(shared.content.subscriptionBlocks, function(block){
block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.months); block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
}); });
paypal.configure({ paypal.configure({
@ -30,23 +32,33 @@ var parseErr = function(res, err){
} }
exports.createBillingAgreement = function(req,res,next){ exports.createBillingAgreement = function(req,res,next){
req.session.paypalBlock = req.query.sub; var sub = shared.content.subscriptionBlocks[req.query.sub];
var block = shared.content.subscriptionBlocks[req.query.sub]; async.waterfall([
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+block.price+' every '+block.months+' months, recurring)'; function(cb){
var billingAgreementAttributes = { if (!sub.discount) return cb(null, null);
"name": billingPlanTitle, if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
"description": billingPlanTitle, mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
"start_date": moment().add({minutes:5}).format(),
"plan": {
"id": block.paypalKey
}, },
"payer": { function(coupon, cb){
"payment_method": "paypal" if (sub.discount && !coupon) return cb('Invalid coupon code.');
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
var billingAgreementAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"start_date": moment().add({minutes:5}).format(),
"plan": {
"id": sub.paypalKey
},
"payer": {
"payment_method": "paypal"
}
};
paypal.billingAgreement.create(billingAgreementAttributes, cb);
} }
}; ], function(err, billingAgreement){
paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) {
if (err) return parseErr(res, err); if (err) return parseErr(res, err);
// For approving subscription via Paypal, first redirect user to: approval_url // For approving subscription via Paypal, first redirect user to: approval_url
req.session.paypalBlock = req.query.sub;
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href; var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
res.redirect(approval_url); res.redirect(approval_url);
}); });
@ -82,10 +94,10 @@ exports.createPayment = function(req, res) {
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined; var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
var price = !gift ? 5.00 var price = !gift ? 5.00
: gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2) : gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2)
: Number(shared.content.subscriptionBlocks[gift.subscription.months].price).toFixed(2); : Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
var description = !gift ? "HabitRPG Gems" var description = !gift ? "HabitRPG Gems"
: gift.type=='gems' ? "HabitRPG Gems (Gift)" : gift.type=='gems' ? "HabitRPG Gems (Gift)"
: gift.subscription.months + "mo. HabitRPG Subscription (Gift)"; : shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
var create_payment = { var create_payment = {
"intent": "sale", "intent": "sale",
"payer": { "payer": {

View file

@ -11,7 +11,7 @@ var paypal = require('paypal-rest-sdk');
var blocks = require('habitrpg-shared').content.subscriptionBlocks; var blocks = require('habitrpg-shared').content.subscriptionBlocks;
var live = nconf.get('PAYPAL:mode')=='live'; var live = nconf.get('PAYPAL:mode')=='live';
var OP = 'get'; // list create update remove var OP = 'create'; // list create update remove
paypal.configure({ paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live 'mode': nconf.get("PAYPAL:mode"), //sandbox or live
@ -72,7 +72,7 @@ switch(OP) {
}); });
break; break;
case "create": case "create":
paypal.billingPlan.create(blocks["12"].definition, function(err,plan){ paypal.billingPlan.create(blocks["google_6mo"].definition, function(err,plan){
if (err) return console.log(err); if (err) return console.log(err);
if (plan.state == "ACTIVE") if (plan.state == "ACTIVE")
return console.log({err:err, plan:plan}); return console.log({err:err, plan:plan});

View file

@ -4,6 +4,8 @@ var async = require('async');
var payments = require('./index'); var payments = require('./index');
var User = require('mongoose').model('User'); var User = require('mongoose').model('User');
var shared = require('habitrpg-shared'); var shared = require('habitrpg-shared');
var mongoose = require('mongoose');
var cc = require('coupon-code');
/* /*
Setup Stripe response when posting payment Setup Stripe response when posting payment
@ -17,16 +19,27 @@ exports.checkout = function(req, res, next) {
async.waterfall([ async.waterfall([
function(cb){ function(cb){
if (sub) { if (sub) {
stripe.customers.create({ async.waterfall([
email: req.body.email, function(cb2){
metadata: {uuid: user._id}, if (!sub.discount) return cb2(null, null);
card: token, if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
plan: sub.key mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
}, cb); },
function(coupon, cb2){
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
var customer = {
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key
};
stripe.customers.create(customer, cb2);
}
], cb);
} else { } else {
stripe.charges.create({ stripe.charges.create({
amount: !gift ? "500" //"500" = $5 amount: !gift ? "500" //"500" = $5
: gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.months].price*100 : gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.key].price*100
: ""+gift.gems.amount/4*100, : ""+gift.gems.amount/4*100,
currency: "usd", currency: "usd",
card: token card: token

View file

@ -257,37 +257,57 @@ api.update = function(req, res, next) {
}; };
api.cron = function(req, res, next) { api.cron = function(req, res, next) {
var user = res.locals.user, try{
progress = user.fns.cron(), var user = res.locals.user,
ranCron = user.isModified(), progress = user.fns.cron(),
quest = shared.content.quests[user.party.quest.key]; ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
if (ranCron) res.locals.wasModified = true;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
if (ranCron) res.locals.wasModified = true; // FOR DEBUGGING, PLEASE IGNORE
if (!ranCron) return next(null,user); var opStatus = null;
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
// If user is on a quest, roll for boss & player, or handle collections // If user is on a quest, roll for boss & player, or handle collections
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this? // FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
async.waterfall([ async.waterfall([
function(cb){ function(cb){
user.save(cb); // make sure to save the cron effects opStatus = 'saveUser';
}, user.save(cb); // make sure to save the cron effects
function(saved, count, cb){ },
var type = quest.boss ? 'boss' : 'collect'; function(saved, count, cb){
Group[type+'Quest'](user,progress,cb); opStatus = 'runQuest';
}, var type = quest.boss ? 'boss' : 'collect';
function(){ Group[type+'Quest'](user,progress,cb);
var cb = arguments[arguments.length-1]; },
// User has been updated in boss-grapple, reload function(){
User.findById(user._id, cb); var cb = arguments[arguments.length-1];
} // User has been updated in boss-grapple, reload
], function(err, saved) { User.findById(user._id, cb);
res.locals.user = saved; }
next(err,saved); ], function(err, saved) {
user = progress = quest = null; if(err) logging.loggly({
}); error: "Cron caught",
stack: (err.stack || err.message || err),
body: req.body, headers: req.header,
auth: (req.headers['x-api-user'] + ' | ' + req.headers['x-api-key']),
originalUrl: req.originalUrl,
opStatus: opStatus
});
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
}catch(e){
logging.loggly({
error: "Cron uncaught",
stack: e.stack || e
});
throw e;
}
}; };

View file

@ -3,7 +3,23 @@ var winston = require('winston');
require('winston-mail').Mail; require('winston-mail').Mail;
require('winston-newrelic'); require('winston-newrelic');
var logger; var logger, loggly;
if (nconf.get('LOGGLY:enabled')){
loggly = require('loggly').createClient({
token: nconf.get('LOGGLY:token'),
subdomain: nconf.get('LOGGLY:subdomain'),
auth: {
username: nconf.get('LOGGLY:username'),
password: nconf.get('LOGGLY:password')
},
//
// Optional: Tag to send with EVERY log message
//
tags: [('heroku-'+nconf.get('BASE_URL'))],
json: true
});
}
if (logger == null) { if (logger == null) {
logger = new (winston.Logger)({}); logger = new (winston.Logger)({});
@ -49,3 +65,8 @@ module.exports.error = function(/* variable args */) {
if (logger) if (logger)
logger.error.apply(logger, arguments); logger.error.apply(logger, arguments);
}; };
module.exports.loggly = function(/* variable args */){
if (loggly)
loggly.log.apply(loggly, arguments);
};

View file

@ -75,6 +75,13 @@ module.exports.errorHandler = function(err, req, res, next) {
"\n\nbody: " + JSON.stringify(req.body) + "\n\nbody: " + JSON.stringify(req.body) +
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : ""); (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
logging.error(stack); logging.error(stack);
logging.loggly({
error: "Uncaught error",
stack: (err.stack || err.message || err),
body: req.body, headers: req.header,
auth: (req.headers['x-api-user'] + ' | ' + req.headers['x-api-key']),
originalUrl: req.originalUrl
});
var message = err.message ? err.message : err; var message = err.message ? err.message : err;
message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length); message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
res.json(500,{err:message}); //res.end(err.message); res.json(500,{err:message}); //res.end(err.message);
@ -173,12 +180,9 @@ module.exports.locals = function(req, res, next) {
language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined); language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
var tavern = require('./models/group').tavern; var tavern = require('./models/group').tavern;
res.locals.habitrpg = { var envVars = _.pick(nconf.get(), 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY'.split(' '));
NODE_ENV: nconf.get('NODE_ENV'), res.locals.habitrpg = _.merge(envVars, {
BASE_URL: nconf.get('BASE_URL'),
GA_ID: nconf.get("GA_ID"),
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')), IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
getManifestFiles: getManifestFiles, getManifestFiles: getManifestFiles,
getBuildUrl: getBuildUrl, getBuildUrl: getBuildUrl,
avalaibleLanguages: i18n.avalaibleLanguages, avalaibleLanguages: i18n.avalaibleLanguages,
@ -193,11 +197,9 @@ module.exports.locals = function(req, res, next) {
siteVersion: siteVersion, siteVersion: siteVersion,
Content: shared.content, Content: shared.content,
mods: require('./models/user').mods, mods: require('./models/user').mods,
FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'),
tavern: tavern, // for world boss tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {} worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
}; });
// Put query-string party invitations into session to be handled later // Put query-string party invitations into session to be handled later
try{ try{

View file

@ -7,7 +7,7 @@ var autoinc = require('mongoose-id-autoinc');
var CouponSchema = new mongoose.Schema({ var CouponSchema = new mongoose.Schema({
_id: {type: String, 'default': cc.generate}, _id: {type: String, 'default': cc.generate},
event: {type:String, enum:['wondercon']}, event: {type:String, enum:['wondercon','google_6mo']},
user: {type: 'String', ref: 'User'} user: {type: 'String', ref: 'User'}
}); });
@ -18,38 +18,34 @@ CouponSchema.statics.generate = function(event, count, callback) {
} }
CouponSchema.statics.apply = function(user, code, next){ CouponSchema.statics.apply = function(user, code, next){
var _coupon,_user; async.auto({
async.waterfall([ get_coupon: function (cb) {
function(cb) {
mongoose.model('Coupon').findById(cc.validate(code), cb); mongoose.model('Coupon').findById(cc.validate(code), cb);
}, },
function(coupon, cb) { apply_coupon: ['get_coupon', function (cb, results) {
_coupon = coupon; if (!results.get_coupon) return cb("Invalid coupon code");
if (!coupon) return cb("Invalid coupon code"); if (results.get_coupon.user) return cb("Coupon already used");
if (coupon.user) return cb("Coupon already used"); switch (results.get_coupon.event) {
switch (coupon.event) {
case 'wondercon': case 'wondercon':
user.items.gear.owned.eyewear_special_wondercon_red = true; user.items.gear.owned.eyewear_special_wondercon_red = true;
user.items.gear.owned.eyewear_special_wondercon_black = true; user.items.gear.owned.eyewear_special_wondercon_black = true;
user.items.gear.owned.back_special_wondercon_black = true; user.items.gear.owned.back_special_wondercon_black = true;
user.items.gear.owned.back_special_wondercon_red = true; user.items.gear.owned.back_special_wondercon_red = true;
user.items.gear.owned.body_special_wondercon_red = true; user.items.gear.owned.body_special_wondercon_red = true;
user.items.gear.owned.body_special_wondercon_black = true; user.items.gear.owned.body_special_wondercon_black = true;
user.items.gear.owned.body_special_wondercon_gold = true; user.items.gear.owned.body_special_wondercon_gold = true;
user.extra = {signupEvent: 'wondercon'}; user.extra = {signupEvent: 'wondercon'};
user.save(cb); user.save(cb);
break; break;
} }
}, }],
function(user, count, cb){ expire_coupon: ['apply_coupon', function (cb, results) {
_user = user; results.get_coupon.user = user._id;
_coupon.user = user._id; results.get_coupon.save(cb);
_coupon.save(cb); }]
} }, function(err, results){
], function(err){
if (err) return next(err); if (err) return next(err);
next(null,_user); next(null,results.apply_coupon[0]);
_coupon = _user = null;
}) })
} }

View file

@ -26,7 +26,10 @@ var GroupSchema = new Schema({
# id: String # id: String
# }] # }]
*/ */
leaderOnly: { // restrict group actions to leader (members can't do them)
challenges: {type:Boolean, 'default':false},
//invites: {type:Boolean, 'default':false}
},
memberCount: {type: Number, 'default': 0}, memberCount: {type: Number, 'default': 0},
challengeCount: {type: Number, 'default': 0}, challengeCount: {type: Number, 'default': 0},
balance: Number, balance: Number,
@ -98,7 +101,9 @@ var chatDefaults = module.exports.chatDefaults = function(msg,user){
id: shared.uuid(), id: shared.uuid(),
text: msg, text: msg,
timestamp: +new Date, timestamp: +new Date,
likes: {} likes: {},
flags: {},
flagCount: 0
}; };
if (user) { if (user) {
_.defaults(message, { _.defaults(message, {
@ -316,6 +321,18 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) {
}) })
} }
GroupSchema.methods.toJSON = function() {
var doc = this.toObject();
if(doc.chat){
doc.chat.forEach(function(msg){
msg.flags = {};
});
}
return doc;
};
module.exports.schema = GroupSchema; module.exports.schema = GroupSchema;
var Group = module.exports.model = mongoose.model("Group", GroupSchema); var Group = module.exports.model = mongoose.model("Group", GroupSchema);
@ -330,4 +347,4 @@ Group.count({_id:'habitrpg'},function(err,ct){
type: 'guild', type: 'guild',
privacy:'public' privacy:'public'
}).save(); }).save();
}) })

View file

@ -49,7 +49,8 @@ var UserSchema = new Schema({
perfect: Number, perfect: Number,
habitBirthday: Boolean, habitBirthday: Boolean,
valentine: Number, valentine: Number,
costumeContest: Boolean costumeContest: Boolean,
nye: Number
}, },
auth: { auth: {
blocked: Boolean, blocked: Boolean,
@ -175,7 +176,9 @@ var UserSchema = new Schema({
snowball: {type: Number, 'default': 0}, snowball: {type: Number, 'default': 0},
spookDust: {type: Number, 'default': 0}, spookDust: {type: Number, 'default': 0},
valentine: Number, valentine: Number,
valentineReceived: Array // array of strings, by sender name valentineReceived: Array, // array of strings, by sender name
nye: Number,
nyeReceived: Array
}, },
// -------------- Animals ------------------- // -------------- Animals -------------------
@ -280,6 +283,7 @@ var UserSchema = new Schema({
automaticAllocation: Boolean, automaticAllocation: Boolean,
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'}, allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
costume: Boolean, costume: Boolean,
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
sleep: {type: Boolean, 'default': false}, sleep: {type: Boolean, 'default': false},
stickyHeader: {type: Boolean, 'default': true}, stickyHeader: {type: Boolean, 'default': true},
disableClasses: {type: Boolean, 'default': false}, disableClasses: {type: Boolean, 'default': false},

View file

@ -577,11 +577,37 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.likeChatMessage action: groups.likeChatMessage
"/groups/{gid}/chat/{mid}/flag":
spec:
method: 'POST'
description: "Flag a chat message"
parameters: [
path 'gid','Group id','string'
path 'mid','Message id','string'
]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.flagChatMessage
"/groups/{gid}/chat/{mid}/clearflags":
spec:
method: 'POST'
description: "Clear flag count from message and unhide it"
parameters: [
path 'gid','Group id','string'
path 'mid','Message id','string'
]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.clearFlagCount
# --------------------------------- # ---------------------------------
# Members # Members
# --------------------------------- # ---------------------------------
"/members/{uuid}": "/members/{uuid}:GET":
spec:{} spec:
path: '/members/{uuid}'
description: "Get a member."
parameters: [path('uuid','Member ID','string')]
middleware: [auth.auth, i18n.getUserLanguage]
action: members.getMember action: members.getMember
"/members/{uuid}/message": "/members/{uuid}/message":
spec: spec:
@ -589,7 +615,7 @@ module.exports = (swagger, v2) ->
description: 'Send a private message to a member' description: 'Send a private message to a member'
parameters: [ parameters: [
path 'uuid', 'The UUID of the member to message', 'string' path 'uuid', 'The UUID of the member to message', 'string'
body '', '{message: "The private message to send"}', 'object' body '', '{"message": "The private message to send"}', 'object'
] ]
middleware: [auth.auth] middleware: [auth.auth]
action: members.sendPrivateMessage action: members.sendPrivateMessage
@ -608,7 +634,7 @@ module.exports = (swagger, v2) ->
description: 'Send a gift to a member' description: 'Send a gift to a member'
parameters: [ parameters: [
path 'uuid', 'The UUID of the member', 'string' path 'uuid', 'The UUID of the member', 'string'
body '', '{gems:{amount:Number, fromBalance:Boolean}, subscription:{months:Number}}', 'object' body '', '{"type": "gems or subscription", "gems":{"amount":Number, "fromBalance":Boolean}, "subscription":{"months":Number}}', 'object'
] ]
middleware: [auth.auth] middleware: [auth.auth]
action: members.sendGift action: members.sendGift

View file

@ -17,4 +17,9 @@ router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.
//router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead //router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead
router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel); router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel);
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
router.post("/iap/ios/verify", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.iapIosVerify);
router.get("/api/v2/coupons/valid-discount/:code", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
module.exports = router; module.exports = router;

View file

@ -35,6 +35,8 @@ function getMailingInfo(user) {
} }
module.exports.txnEmail = function(mailingInfo, emailType, variables){ module.exports.txnEmail = function(mailingInfo, emailType, variables){
var variables = [{name: 'BASE_URL', content: nconf.get('BASE_URL')}].concat(variables || []);
if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo); if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo);
if (!mailingInfo.email) return; if (!mailingInfo.email) return;
request({ request({

View file

@ -141,7 +141,7 @@ describe "API", ->
id: res.body.todos[0].id id: res.body.todos[0].id
body: body:
dateCompleted: moment().subtract("days", 4) dateCompleted: moment().subtract(4, "days")
} }
{ {
op: "updateTask" op: "updateTask"
@ -149,7 +149,7 @@ describe "API", ->
id: res.body.todos[1].id id: res.body.todos[1].id
body: body:
dateCompleted: moment().subtract("days", 4) dateCompleted: moment().subtract(4, "days")
} }
]).end (res) -> ]).end (res) ->
expect(_.size(res.body.todos)).to.be 4 expect(_.size(res.body.todos)).to.be 4
@ -460,7 +460,7 @@ describe "API", ->
{ {
op: "update" op: "update"
body: body:
lastCron: moment().subtract("days", 1) lastCron: moment().subtract(1, "days")
} }
]).end (res) -> ]).end (res) ->
expect(res.body.party.quest.progress.up).to.be.above up expect(res.body.party.quest.progress.up).to.be.above up
@ -532,7 +532,7 @@ describe "API", ->
{ {
op: "update" op: "update"
body: body:
lastCron: moment().subtract("days", 1) lastCron: moment().subtract(1, "days")
} }
]).end -> ]).end ->
cb() cb()
@ -608,7 +608,7 @@ describe "API", ->
it "Handles unsubscription", (done) -> it "Handles unsubscription", (done) ->
cron = -> cron = ->
user.lastCron = moment().subtract("d", 1) user.lastCron = moment().subtract(1, "d")
user.fns.cron() user.fns.cron()
expect(user.purchased.plan.customerId).to.not.be.ok() expect(user.purchased.plan.customerId).to.not.be.ok()
@ -624,7 +624,7 @@ describe "API", ->
cron() cron()
expect(user.purchased.plan.customerId).to.be.ok() expect(user.purchased.plan.customerId).to.be.ok()
expect(user.purchased.plan.dateTerminated).to.be.ok() expect(user.purchased.plan.dateTerminated).to.be.ok()
user.purchased.plan.dateTerminated = moment().subtract("d", 2) user.purchased.plan.dateTerminated = moment().subtract(2, "d")
cron() cron()
expect(user.purchased.plan.customerId).to.not.be.ok() expect(user.purchased.plan.customerId).to.not.be.ok()
payments.createSubscription user, payments.createSubscription user,

View file

@ -15,9 +15,12 @@ script(type='text/ng-template', id='partials/options.inventory.html')
li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.equipment') }") li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.equipment') }")
a(ui-sref='options.inventory.equipment') a(ui-sref='options.inventory.equipment')
=env.t('equipment') =env.t('equipment')
li.equipment-tab(ng-class="{ active: $state.includes('options.inventory.timetravelers') }") li(ng-class="{ active: $state.includes('options.inventory.timetravelers') }")
a(ui-sref='options.inventory.timetravelers') a(ui-sref='options.inventory.timetravelers')
| Time Travelers =env.t('timeTravelers')
li(ng-class="{ active: $state.includes('options.inventory.seasonalshop') }")
a(ui-sref='options.inventory.seasonalshop')
=env.t('seasonalShop')
.tab-content .tab-content
.tab-pane.active .tab-pane.active

View file

@ -19,6 +19,42 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
div(ng-repeat='item in gear[klass]') div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}') button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{::item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
script(type='text/ng-template', id='partials/options.inventory.seasonalshop.html')
.container-fluid
.stable.row
.col-md-2
.seasonalshop_winter2015
.col-md-10
.popover.static-popover.fade.right.in
.arrow
h3.popover-title!=env.t('seasonalShopTitle', {linkStart:"<a href='http://blog.habitrpg.com/who' target='_blank'>", linkEnd: "</a>"})
.popover-content
p!=env.t('seasonalShopText')
li.customize-menu.inventory-gear
menu.pets-menu(label='{{::label}}', ng-repeat='(set,label) in ::{candycane:env.t("candycaneSet"), ski:env.t("skiSet"), snowflake:env.t("snowflakeSet"), yeti:env.t("yetiSet")}')
div(ng-repeat='item in ::getSeasonalShopArray(set)' ng-class="{transparent: user.items.gear.owned[item.key]}")
button.customize-option(popover='{{::item.notes()}}', popover-title='{{::item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='purchase(item.type,item)', class='shop_{{::item.key}}')
.text-center
| {{((item.specialClass == "wizard") && (item.type == "weapon")) + 1}}&nbsp;
span.Pet_Currency_Gem1x.inline-gems
menu.pets-menu(label=env.t('quests'))
div(ng-repeat='quest in ::getSeasonalShopQuests()')
button.customize-option(popover="{{::quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes() | htmlDecode}}", popover-title='{{::quest.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='buyQuest(quest.key)', class='inventory_quest_scroll', ng-class='::{locked: quest.previous && !user.achievements.quests[quest.previous]}')
p
| {{::quest.value}}&nbsp;
span.Pet_Currency_Gem1x.inline-gems
menu.pets-menu(label=env.t('seasonalItems'))
div
button.customize-option(popover='{{::Content.spells.special.snowball.notes()}}', popover-title='{{::Content.spells.special.snowball.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='purchase("special", Content.spells.special.snowball)', class='inventory_special_snowball')
p
| {{::Content.spells.special.snowball.value}}
span(class='shop_gold')
div
button.customize-option(popover='{{::Content.spells.special.nye.notes()}}', popover-title='{{::Content.spells.special.nye.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='castStart(Content.spells.special.nye)', class='inventory_special_nye')
p
| {{Content.spells.special.nye.value}}
span(class='shop_gold')
script(type='text/ng-template', id='partials/options.inventory.timetravelers.html') script(type='text/ng-template', id='partials/options.inventory.timetravelers.html')
.container-fluid .container-fluid
.stable.row(ng-if='user.purchased.plan.consecutive.trinkets <= 0') .stable.row(ng-if='user.purchased.plan.consecutive.trinkets <= 0')
@ -83,7 +119,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
li.customize-menu li.customize-menu
menu.pets-menu(label=env.t('food') + ' ({{foodCount}})') menu.pets-menu(label=env.t('food') + ' ({{foodCount}})')
p(ng-show='foodCount < 1')=env.t('noFood') p.muted(ng-show='foodCount < 1')=env.t('noFood')
div(ng-repeat='(food,points) in ownedItems(user.items.food)') div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover='{{::Content.food[food].notes()}}', popover-title='{{::Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}') button.customize-option(popover='{{::Content.food[food].notes()}}', popover-title='{{::Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}')
.badge.badge-info.stack-count {{points}} .badge.badge-info.stack-count {{points}}
@ -98,12 +134,10 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
+specialItem('snowball') +specialItem('snowball')
+specialItem('spookDust') +specialItem('spookDust')
//-menu.pets-menu(label=env.t('valentineCard'))
div(ng-if='user.items.special.valentineReceived[0]') div(ng-if='user.items.special.valentineReceived[0]')
button.customize-option(popover="Valentine's Day Card from {{User.user.items.special.valentineReceived[0]}}", popover-trigger='mouseenter', popover-placement='right', ng-click='openModal("valentine")', class='inventory_special_valentine') button.customize-option(popover="Valentine's Day Card from {{User.user.items.special.valentineReceived[0]}}", popover-trigger='mouseenter', popover-placement='right', ng-click='openModal("valentine")', class='inventory_special_valentine')
.badge.badge-info.stack-count {{user.items.special.valentineReceived.length}} .badge.badge-info.stack-count {{user.items.special.valentineReceived.length}}
//-menu.pets-menu(label=env.t('subscriberItem'))
div(ng-if='user.purchased.plan.customerId || user.purchased.plan.mysteryItems.length') div(ng-if='user.purchased.plan.customerId || user.purchased.plan.mysteryItems.length')
button.customize-option(popover=env.t('subscriberItemText'), popover-trigger='mouseenter', popover-placement='right', class='inventory_present', ng-click="user.ops.openMysteryItem({})") button.customize-option(popover=env.t('subscriberItemText'), popover-trigger='mouseenter', popover-placement='right', class='inventory_present', ng-click="user.ops.openMysteryItem({})")
.badge.badge-info.stack-count {{user.purchased.plan.mysteryItems.length}} .badge.badge-info.stack-count {{user.purchased.plan.mysteryItems.length}}
@ -112,6 +146,10 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
button.customize-option(popover=env.t('mysticHourglassPopover'), popover-trigger='mouseenter', popover-placement='right', class='inventory_special_trinket', ng-click="$state.go('options.inventory.timetravelers')") button.customize-option(popover=env.t('mysticHourglassPopover'), popover-trigger='mouseenter', popover-placement='right', class='inventory_special_trinket', ng-click="$state.go('options.inventory.timetravelers')")
.badge.badge-info.stack-count {{user.purchased.plan.consecutive.trinkets}} .badge.badge-info.stack-count {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='user.items.special.nyeReceived[0]')
button.customize-option(popover="New Year's Card from {{User.user.items.special.nyeReceived[0]}}", popover-trigger='mouseenter', popover-placement='right', ng-click='openModal("nye")', class='inventory_special_nye')
.badge.badge-info.stack-count {{user.items.special.nyeReceived.length}}
.col-md-6 .col-md-6
h2=env.t('market') h2=env.t('market')
.row-fluid .row-fluid
@ -178,15 +216,6 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p p
| 4 | 4
span.Pet_Currency_Gem1x.inline-gems span.Pet_Currency_Gem1x.inline-gems
mixin buySpecialSpell(k,gp)
div
button.customize-option(popover='{{::Content.spells.special.#{k}.notes()}}', popover-title='{{::Content.spells.special.#{k}.text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='purchase("special", Content.spells.special.#{k})', class='inventory_special_#{k}')
p
| {{::Content.spells.special.#{k}.value}}
span(class='#{gp ? "shop_gold" : "Pet_Currency_Gem1x inline-gems"}')
//-+buySpecialSpell('snowball',false)
//-+buySpecialSpell('spookDust',true)
div(ng-show='user.flags.rebirthEnabled') div(ng-show='user.flags.rebirthEnabled')
button.customize-option(popover=env.t('rebirthPop'), popover-title=env.t('rebirthName'), popover-trigger='mouseenter', popover-placement='top', ng-click='openModal("rebirth")', class='rebirth_orb') button.customize-option(popover=env.t('rebirthPop'), popover-title=env.t('rebirthName'), popover-trigger='mouseenter', popover-placement='top', ng-click='openModal("rebirth")', class='rebirth_orb')
p(ng-show='user.stats.lvl < 100') p(ng-show='user.stats.lvl < 100')
@ -197,7 +226,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p p
| 4 | 4
span.Pet_Currency_Gem1x.inline-gems span.Pet_Currency_Gem1x.inline-gems
div(ng-if='user.purchased.plan.customerId') div(ng-if='user.purchased.plan.customerId', ng-class='::{transparent:(Shared.planGemLimits.convCap + User.user.purchased.plan.consecutive.gemCapExtra - User.user.purchased.plan.gemsBought) < 1}')
button.customize-option(popover=env.t('subGemPop'), popover-title=env.t('subGemName'), popover-trigger='mouseenter', popover-placement='top',ng-click='user.ops.purchase({params:{type:"gems",key:"gem"}})') button.customize-option(popover=env.t('subGemPop'), popover-title=env.t('subGemName'), popover-trigger='mouseenter', popover-placement='top',ng-click='user.ops.purchase({params:{type:"gems",key:"gem"}})')
span.Pet_Currency_Gem.inline-gems span.Pet_Currency_Gem.inline-gems
.badge.badge-success.stack-count {{Shared.planGemLimits.convCap + User.user.purchased.plan.consecutive.gemCapExtra - User.user.purchased.plan.gemsBought}} .badge.badge-success.stack-count {{Shared.planGemLimits.convCap + User.user.purchased.plan.consecutive.gemCapExtra - User.user.purchased.plan.gemsBought}}

View file

@ -15,8 +15,9 @@ mixin customizeProfile(mobile)
if ~['limited','seasonal'].indexOf(status) if ~['limited','seasonal'].indexOf(status)
.label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition') .label.label-info.pull-right.hint(popover=limited, popover-title=env.t(status+'Edition'), popover-placement='right', popover-trigger='mouseenter')=env.t(status+'Edition')
menu(label=env.t(title)) menu(label=env.t(title))
+gemCost(2) span(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "&&")}')
button.btn.btn-xs(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "||")}', ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' +gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath(path, colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each color in colors each color in colors
button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")') button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")')
@ -38,8 +39,9 @@ mixin customizeProfile(mobile)
menu(label=env.t('specialShirts')) menu(label=env.t('specialShirts'))
- var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie'] - var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie']
+gemCost(2) span(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}')
button.btn.btn-xs(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}', ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' +gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath("shirt",specialShirts)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each shirt in specialShirts each shirt in specialShirts
button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")') button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")')
@ -61,6 +63,7 @@ mixin customizeProfile(mobile)
button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair hair_bangs_1_#{color}', ng-click='unlock("hair.color.#{color}")') button(type='button', ng-if='user.purchased.hair.color.#{color}', class='customize-option hair hair_bangs_1_#{color}', ng-click='unlock("hair.color.#{color}")')
+buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors') +buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors')
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled') +buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled')
+buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors')
li.customize-menu li.customize-menu
menu(label=env.t('bodyHair')) menu(label=env.t('bodyHair'))
@ -80,8 +83,9 @@ mixin customizeProfile(mobile)
// Purchasable hairstyles // Purchasable hairstyles
menu(label=env.t('hairSet1')) menu(label=env.t('hairSet1'))
- var colors = [2,4,5,6,7,8] - var colors = [2,4,5,6,7,8]
+gemCost(2) span(ng-hide='#{showPath("user.purchased.hair.base", colors, "&&")}')
button.btn.btn-xs(ng-hide='#{showPath("user.purchased.hair", colors, "&&")}', ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' +gemCost(2)
button.btn.btn-xs(ng-click='#{unlockPath("hair.base",colors)}')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each num in colors each num in colors
button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")') button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base["#{num}"]}', ng-click='unlock("hair.base.#{num}")')
@ -93,9 +97,10 @@ mixin customizeProfile(mobile)
button(class='hair_flower_#{num} customize-option', type='button', ng-click='set({"preferences.hair.flower":#{num}})') button(class='hair_flower_#{num} customize-option', type='button', ng-click='set({"preferences.hair.flower":#{num}})')
li.customize-menu li.customize-menu
h5=env.t('bodyFacialHair') menu(label=env.t('bodyFacialHair'))
+gemCost(2) span(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]')
button.btn.btn-xs(ng-hide='user.purchased.hair.mustache["1"] && user.purchased.hair.mustache["2"] && user.purchased.hair.beard["1"] && user.purchased.hair.beard["2"] && user.purchased.hair.beard["3"]', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' +gemCost(2)
button.btn.btn-xs(ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
// Beard // Beard
menu(label=env.t('beard')) menu(label=env.t('beard'))
@ -249,9 +254,9 @@ mixin backgrounds(mobile)
- var k = bgsKeys[i], bgs = env.Content.backgrounds[k]; - var k = bgsKeys[i], bgs = env.Content.backgrounds[k];
li.customize-menu li.customize-menu
menu(label=env.t(k)) menu(label=env.t(k))
+gemCost(7) span(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})")
//-button.btn.btn-xs(ng-hide="ownsSet('background',Content.backgrounds['#{k}'])",ng-click="unlock(setKeys('background',Content.backgrounds['#{k}']))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' +gemCost(7)
button.btn.btn-xs(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})",ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>' button.btn.btn-xs(ng-click="unlock(setKeys('background',#{JSON.stringify(bgs)}))")!= env.t('unlockSet',{cost:15}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each bg,k in bgs each bg,k in bgs
button.customize-option(type='button', class='background_#{k}', ng-class="user.purchased.background.#{k} ? 'background-unlocked' : 'background-locked'", ng-click='unlock("background.#{k}")', popover-title=bg.text(), popover=bg.notes(),popover-trigger='mouseenter') button.customize-option(type='button', class='background_#{k}', ng-class="user.purchased.background.#{k} ? 'background-unlocked' : 'background-locked'", ng-click='unlock("background.#{k}")', popover-title=bg.text(), popover=bg.notes(),popover-trigger='mouseenter')
i.glyphicon.glyphicon-lock(ng-if="!user.purchased.background.#{k}") i.glyphicon.glyphicon-lock(ng-if="!user.purchased.background.#{k}")

View file

@ -36,6 +36,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
br br
strong strong
!=env.t('helpWithTranslation') !=env.t('helpWithTranslation')
.form-horizontal
h5=env.t('dateFormat')
select.form-control(ng-model='user.preferences.dateFormat', ng-options='DF for DF in availableFormats', ng-change='set({"preferences.dateFormat": user.preferences.dateFormat})')
.checkbox .checkbox
label label
input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true') input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true')
@ -200,7 +204,7 @@ mixin subPerks()
tr tr
td td
span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}&nbsp; span.hint(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right') #{env.t('buyGemsGold')}&nbsp;
span.badge.badge-success(ng-show='_subscription.months>1') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(_subscription.months/3*5), 50] | min }} span.badge.badge-success(ng-show='_subscription.key!="basic_earned"') Cap raised to {{ [25 + user.purchased.plan.consecutive.gemCapExtra + Math.floor(Content.subscriptionBlocks[_subscription.key].months/3*5), 50] | min }}
tr tr
td td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory') span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@ -210,8 +214,8 @@ mixin subPerks()
tr tr
td td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}&nbsp; span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}&nbsp;
div(ng-show='_subscription.months>1') div(ng-show='_subscription.key!="basic_earned"')
.badge.badge-success +{{Math.floor(_subscription.months/3)}} Mystic Hourglass .badge.badge-success +{{Math.floor(Content.subscriptionBlocks[_subscription.key].months/3)}} Mystic Hourglass
.small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set. .small.muted Mystic Hourglasses allow purchasing a previous month's Mystery Item set.
tr tr
td td
@ -224,7 +228,7 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template')
script(id='partials/options.settings.subscription.html',type='text/ng-template') script(id='partials/options.settings.subscription.html',type='text/ng-template')
//-h2=env.t('individualSub') //-h2=env.t('individualSub')
.container-fluid(ng-init='_subscription={months:1}') .container-fluid(ng-init='_subscription={key:"basic_earned"}')
.row .row
.col-md-6 .col-md-6
h3 Benefits h3 Benefits
@ -237,7 +241,7 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong> | #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed') h4=env.t('subscribed')
p(ng-if='Payments.currentSub') Recurring ${{Payments.currentSub.price}} each {{Payments.currentSub.months}} Month(s) ({{user.purchased.plan.paymentMethod}}) p(ng-if='user.purchased.plan.planId') Recurring ${{Content.subscriptionBlocks[user.purchased.plan.planId].price}} each {{Content.subscriptionBlocks[user.purchased.plan.planId].months}} Month(s) ({{user.purchased.plan.paymentMethod}})
tr(ng-if='user.purchased.plan.extraMonths'): td tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card span.glyphicon.glyphicon-credit-card
| You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit. | You have {{user.purchased.plan.extraMonths | number:2}} months of subscription credit.
@ -250,16 +254,23 @@ script(id='partials/options.settings.subscription.html',type='text/ng-template')
li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}} li Mystic Hourglasses: {{user.purchased.plan.consecutive.trinkets}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group .form-group
each block in env.Content.subscriptionBlocks .radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
.radio label
label input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
input(type="radio", name="subRadio", value="#{block.months}", ng-model='_subscription.months') span(ng-show='block.original')
//-| #{block.months} Month(s) Recurring: $#{block.price} #{env.t('monthUSD')} | Recurring <span class='line-through'>${{block.original}}</span> <span class='label label-success'>${{block.price}}</span> each {{block.months}} Month(s)
| Recurring $#{block.price} each #{block.months} Month(s) span(ng-hide='block.original')
| Recurring ${{block.price}} each {{block.months}} Month(s)
.form-inline
.form-group
input.form-control(type='text', ng-model='_subscription.coupon', placeholder='Promo Code')
.form-group
button.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)') Apply
h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') Resubscribe h3(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)') Resubscribe
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.months})', ng-disabled='!_subscription.months') Card a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key') Card
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.months}}', ng-disabled='!_subscription.months') PayPal a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
div(ng-if='user.purchased.plan.customerId') div(ng-if='user.purchased.plan.customerId')
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()') Update Card .btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub') .btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')

View file

@ -1,17 +1,20 @@
mixin chatMessages(inbox) mixin chatMessages(inbox)
ul.list-unstyled.tavern-chat ul.list-unstyled.tavern-chat
- var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id' - var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id'
li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', style='{{::message.sent ? "opacity:0.5" : ""}}') li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', ng-if="!message.flagCount || message.flagCount < 2 || user.contributor.admin")
.scrollable-message span.pull-right.text-danger(ng-if="user.contributor.admin && message.flagCount > 0")
| {{message.flagCount > 1 ? "Message Hidden" : "1 flag"}}
.scrollable-message(ng-class='{"transparent": message.sent || message.flags[user._id] || (user.contributor.admin && message.flagCount > 1)}')
span(ng-if='::message.user') span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label a.label.label-default.chat-message.hidden-label
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
| &nbsp; &nbsp;
span {{::message.user}}&nbsp; span {{::message.user}}&nbsp;
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)') span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
// this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text // this invisible username label is here to push the message text far enough right that the visible label can be floated to this point without covering up any of the text
markdown(ng-model='::message.text') markdown(ng-model='::message.text')
| - | -
span.muted.time(from-now='::message.timestamp' tooltip="{{::message.timestamp | date:'short'}}") span.muted.time(from-now='::message.timestamp' tooltip="{{::message.timestamp | date:user.preferences.dateFormat.concat(' HH:mm:ss')}}")
unless inbox unless inbox
span span
a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}} a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}}
@ -22,11 +25,15 @@ mixin chatMessages(inbox)
if inbox if inbox
a(ng-click="quickReply(message.uuid)") a(ng-click="quickReply(message.uuid)")
span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply'))
| &nbsp; &nbsp; &nbsp; &nbsp; | &nbsp; &nbsp;
a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
span.float-label | &nbsp; &nbsp;
a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")')
span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""')
span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""')
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
| &nbsp;
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}&nbsp; span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}&nbsp;
span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)') span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)')

View file

@ -19,5 +19,10 @@ form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
span.gem-cost= '4 ' + env.t('gems') span.gem-cost= '4 ' + env.t('gems')
p p
small=env.t('gemCost') small=env.t('gemCost')
.form-group
.checkbox
label
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
| Only group leader can create challenges
.form-group(ng-show='type=="party"') .form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create')) input.btn.btn-default.form-control(type='submit', value=env.t('create'))

View file

@ -33,6 +33,11 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
.form-group .form-group
label=env.t('logoUrl') label=env.t('logoUrl')
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo') input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo')
.form-group
.checkbox
label
input(type='checkbox', ng-model='group.leaderOnly.challenges')
| Only group leader can create challenges
h4=env.t('assignLeader') h4=env.t('assignLeader')
select#group-leader-selection(ng-model='group._newLeader', ng-options='member.profile.name for member in group.members') select#group-leader-selection(ng-model='group._newLeader', ng-options='member.profile.name for member in group.members')

View file

@ -16,6 +16,7 @@ script(type='text/ng-template', id='partials/options.social.inbox.html')
.checkbox .checkbox
label label
input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})') input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})')
| &nbsp;
span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('optOutPopover'))=env.t('optOut') span.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('optOutPopover'))=env.t('optOut')
script(type='text/ng-template', id='partials/options.social.tavern.html') script(type='text/ng-template', id='partials/options.social.tavern.html')
@ -104,13 +105,13 @@ script(type='text/ng-template', id='partials/options.social.html')
a(ui-sref='options.social.party') a(ui-sref='options.social.party')
=env.t('party') =env.t('party')
li(ng-class="{ active: $state.includes('options.social.guilds') }") li(ng-class="{ active: $state.includes('options.social.guilds') }")
a(ui-sref='options.social.guilds') a(ui-sref='options.social.guilds.public')
=env.t('guilds') =env.t('guilds')
li(ng-class="{ active: $state.includes('options.social.challenges') }") li(ng-class="{ active: $state.includes('options.social.challenges') }")
a(ui-sref='options.social.challenges') a(ui-sref='options.social.challenges')
=env.t('challenges') =env.t('challenges')
li(ng-class="{ active: $state.includes('options.social.hall') }") li(ng-class="{ active: $state.includes('options.social.hall') }")
a(ui-sref='options.social.hall') a(ui-sref='options.social.hall.heroes')
=env.t('hall') =env.t('hall')
.tab-content .tab-content

View file

@ -21,7 +21,7 @@
span(ng-hide='user.preferences.sleep')=env.t('innCheckIn') span(ng-hide='user.preferences.sleep')=env.t('innCheckIn')
=env.t('danielText2') =env.t('danielText2')
.alert.alert-info(ng-show='user.preferences.sleep') .alert.alert-info(ng-show='user.preferences.sleep')
=env.t('innText') =env.t('innText',{name:"{{user.profile.name}}"})
// Resources // Resources
.panel.panel-default .panel.panel-default

View file

@ -34,11 +34,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li li
a(ui-sref='options.social.party')=env.t('party') a(ui-sref='options.social.party')=env.t('party')
li li
a(ui-sref='options.social.guilds')=env.t('guilds') a(ui-sref='options.social.guilds.public')=env.t('guilds')
li li
a(ui-sref='options.social.challenges')=env.t('challenges') a(ui-sref='options.social.challenges')=env.t('challenges')
li li
a(ui-sref='options.social.hall')=env.t('hall') a(ui-sref='options.social.hall.heroes')=env.t('hall')
ul.toolbar-submenu ul.toolbar-submenu
li li
a(ui-sref='options.inventory.drops')=env.t('market') a(ui-sref='options.inventory.drops')=env.t('market')
@ -49,7 +49,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li li
a(ui-sref='options.inventory.equipment')=env.t('equipment') a(ui-sref='options.inventory.equipment')=env.t('equipment')
li li
a(ui-sref='options.inventory.timetravelers') Time Travelers a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
li
a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
ul.toolbar-submenu ul.toolbar-submenu
li li
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool') a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
@ -110,11 +112,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li li
a(ui-sref='options.social.party')=env.t('party') a(ui-sref='options.social.party')=env.t('party')
li li
a(ui-sref='options.social.guilds')=env.t('guilds') a(ui-sref='options.social.guilds.public')=env.t('guilds')
li li
a(ui-sref='options.social.challenges')=env.t('challenges') a(ui-sref='options.social.challenges')=env.t('challenges')
li li
a(ui-sref='options.social.hall')=env.t('hall') a(ui-sref='options.social.hall.heroes')=env.t('hall')
li.toolbar-button-dropdown li.toolbar-button-dropdown
a(ui-sref='options.inventory.drops') a(ui-sref='options.inventory.drops')
span=env.t('inventory') span=env.t('inventory')
@ -131,7 +133,9 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li li
a(ui-sref='options.inventory.equipment')=env.t('equipment') a(ui-sref='options.inventory.equipment')=env.t('equipment')
li li
a(ui-sref='options.inventory.timetravelers') Time Travelers a(ui-sref='options.inventory.timetravelers')=env.t('timeTravelers')
li
a(ui-sref='options.inventory.seasonalshop')=env.t('seasonalShop')
li.toolbar-button-dropdown li.toolbar-button-dropdown
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}') a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')
span=env.t('data') span=env.t('data')

View file

@ -27,4 +27,30 @@ script(id='modals/valentine.html', type='text/ng-template')
p p
small For enduring such a saccharine poem, you both receive the "Adoring Friends" badge! small For enduring such a saccharine poem, you both receive the "Adoring Friends" badge!
.modal-footer .modal-footer
button.btn.btn-default(ng-click='user.ops.readValentine({});$close()')=env.t('ok') button.btn.btn-default(ng-click='user.ops.readValentine({});$close()')=env.t('ok')
// New Year's
script(id='modals/nye.html', type='text/ng-template')
.modal-header
h4
.inventory_special_nye.pull-right
=env.t('nyeCard')
.modal-body
.bg-info(style='padding:10px')
p To: {{user.profile.name}}, From: {{user.items.special.nyeReceived[0]}}
hr
ul.list-unstyled(ng-switch='::Math.floor(Math.random()*5)')
li(ng-switch-when='0')
=env.t('newYear0')
li(ng-switch-when='1')
=env.t('newYear1')
li(ng-switch-when='2')
=env.t('newYear2')
li(ng-switch-when='3')
=env.t('newYear3')
li(ng-switch-default)
=env.t('newYear4')
p
small For celebrating the new year together, you both receive the "Auld Acquaintance" badge!
.modal-footer
button.btn.btn-default(ng-click='user.ops.readNYE({});$close()')=env.t('ok')

View file

@ -15,12 +15,12 @@ script(type='text/ng-template', id='modals/member.html')
|&nbsp; |&nbsp;
=env.t('memberSince') =env.t('memberSince')
|&nbsp; |&nbsp;
| {{::timestamp(profile.auth.timestamps.created)}} - | {{::profile.auth.timestamps.created | date:user.preferences.dateFormat}} -
li(ng-show='::profile.auth.timestamps.loggedin') li(ng-show='::profile.auth.timestamps.loggedin')
|&nbsp; |&nbsp;
=env.t('lastLoggedIn') =env.t('lastLoggedIn')
|&nbsp; |&nbsp;
| {{::timestamp(profile.auth.timestamps.loggedin)}} - | {{::profile.auth.timestamps.loggedin | date:user.preferences.dateFormat}} -
h3=env.t('stats') h3=env.t('stats')
.label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }} .label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
include ../profiles/stats include ../profiles/stats
@ -46,7 +46,7 @@ script(type='text/ng-template', id='modals/private-message.html')
.modal-header .modal-header
h4=env.t('pmHeading', {name: "{{profile.profile.name}}"}) h4=env.t('pmHeading', {name: "{{profile.profile.name}}"})
.modal-body .modal-body
textarea.form-control(type='text',ng-model='_message') textarea.form-control(type='text',rows='5',ng-model='_message')
.modal-footer .modal-footer
button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)')=env.t("send") button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)')=env.t("send")
button.btn.btn-default(ng-click='$close()')=env.t('cancel') button.btn.btn-default(ng-click='$close()')=env.t('cancel')
@ -75,11 +75,10 @@ script(type='text/ng-template', id='modals/send-gift.html')
.panel-heading Subscription .panel-heading Subscription
.panel-body .panel-body
.form-group .form-group
each block in env.Content.subscriptionBlocks .radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
.radio label
label input(type="radio", name="subRadio", ng-value="block.key", ng-model='gift.subscription.key')
input(type="radio", name="subRadio", value="#{block.months}", ng-model='gift.subscription.months') | {{block.months}} Month(s): ${{block.price}}
| #{block.months} Month(s): $#{block.price}
textarea.form-control(rows='3', ng-model='gift.message', placeholder='Personal message (optional)') textarea.form-control(rows='3', ng-model='gift.message', placeholder='Personal message (optional)')
@ -88,4 +87,17 @@ script(type='text/ng-template', id='modals/send-gift.html')
button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send") button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send")
a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card') a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card')
a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal
button.btn.btn-default(ng-click='$close()')=env.t('cancel') button.btn.btn-default(ng-click='$close()')=env.t('cancel')
script(type='text/ng-template', id='modals/abuse-flag.html')
.modal-header
h4!=env.t('abuseFlagModalHeading', {name: "<span class='text-danger'>{{profile.profile.name}}</span>"})
.modal-body
blockquote
markdown(ng-model="abuseObject.text")
p!=env.t('abuseFlagModalBody', {firstLinkStart: "<a href='/static/community-guidelines' target='_blank'>", secondLinkStart: "<a href='/static/terms' target='_blank'>", linkEnd: "</a>"})
.modal-footer
button.pull-left.btn.btn-danger(ng-click='clearFlagCount(abuseObject, groupId)', ng-if='user.contributor.admin && abuseObject.flagCount >= 2')
| Reset Flag Count
button.btn.btn-primary(ng-click='$close()')=env.t('cancel')
button.btn.btn-default(ng-click='reportAbuse(user, abuseObject, groupId)')=env.t("abuseFlagModalButton")

View file

@ -5,6 +5,7 @@ script(type='text/ng-template', id='modals/questCompleted.html')
h4 "{{::Content.quests[user.party.quest.completed].text()}}" h4 "{{::Content.quests[user.party.quest.completed].text()}}"
=env.t('completed') =env.t('completed')
.modal-body .modal-body
div(ng-class='::Content.quests[user.party.quest.completed].completion() ? "pull-right-sm" : ""', class='col-centered quest_{{user.party.quest.completed}}')
p(ng-bind-html='::Content.quests[user.party.quest.completed].completion()') p(ng-bind-html='::Content.quests[user.party.quest.completed].completion()')
quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived')) quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived'))
.modal-footer .modal-footer

View file

@ -1,19 +1,114 @@
h5 PENGUIN PET QUEST AND WINTER PLOT-LINE h5 NEW YEAR'S EVE CELEBRATION: Party Hats, New Year's Cards, Snowballs, and Last Chance for Penguin Item Set
tr tr
td td
h5 Penguin Pet Quest h5 Party Hats
.quest_penguin.pull-right .promo_partyhats.pull-right
p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own! p In honor of the new year, some free Party Hats are available in the Rewards store! New users get the ever-handsome Absurd Party Hat, and users who already received one last year get the Silly Party Hat. These hats will be available to purchase until January 31st, but once you've bought them, you'll have them forever. Enjoy!
p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon p.small.muted by Lemoness and SabreCat
tr tr
td td
h5 Winter Plot-Line h5 New Year's Cards (Until Jan 1st Only!)
p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us! .inventory_special_nye.pull-right
p Until January 1st only, the <a href='https://habitrpg.com/#/options/inventory/seasonalshop' target='_blank'>Seasonal Shop</a> is stocking New Year's Cards! Now you can send cards to your friends (and yourself) to wish them a Happy Habit New Year. All senders and recipients will receive the Auld Acquaintance badge! When you receive a card, it will appear in your <a href='https://habitrpg.com/#/options/inventory/drops' target='_blank'>Inventory</a>. Click it to receive a seasonal message!
p.small.muted by Lemoness and SabreCat
tr
td
h5 Snowballs
.inventory_special_snowball.pull-right
p The <a href='https://habitrpg.com/#/options/inventory/seasonalshop' target='_blank'>Seasonal Shop</a> is also stocking Snowballs for gold! Throw them at your friends to have an exciting effect. Anyone hit with a snowball earns the Annoying Friends badge. The results of being hit with a Snowball will last until the end of your day, but you can also reverse them early by buying Salt from the Rewards column. Snowballs are available until January 31st.
p.small.muted by Shaner, Lemoness, and SabreCat
tr
td
h5 Last Chance for Penguin Item Set
p Reminder: this is the final day to <a href='https://habitrpg.com/#/options/settings/subscription' target='_blank'>subscribe</a> and receive the Penguin Item Set! If you want the Penguin Hat or the Penguin Suit, now's the time! Thanks so much for your support <3
hr hr
a(href='/static/old-news', target='_blank') Read older news a(href='/static/old-news', target='_blank') Read older news
mixin oldNews mixin oldNews
h5 12/25/2014 - December Subscriber Item Set and Seasonal Shop! Plus, Flagging Posts and Winter Plot-Line Continues
tr
td
h5 December Subscriber Item Set
.promo_mystery_201412.pull-right
p The December Subscriber Item has been revealed: the Penguin Item Set! All December subscribers will receive the Penguin Hat and the Penguin Suit. You still have six days to <a href='https://habitrpg.com/#/options/settings/subscription' target='_blank'>subscribe</a>) and receive the item set! Thank you so much for your support - we really do rely on you to keep HabitRPG free to use and running smoothly.
p.small.muted by Lemoness
tr
td
h5 Seasonal Shop: Seasonal Outfits and Quests
.seasonalshop_winter2015.pull-right
p The <a href='https://habitrpg.com/#/options/inventory/seasonalshop'>Seasonal Shop</a> has opened! The Seasonal Sorceress is stocking the seasonal edition versions of last year's winter outfits, now available for Gems instead of Gold, and the two winter quests, Trapper Santa and Find the Cub. The Seasonal Shop will only be open until January 31st, so don't wait!
p.small.muted by SabreCat and Lemoness
tr
td
h5 Flagging Posts
p You can now report inappropriate posts to moderators simply by clicking the new flag button next to the post. You should only report posts that violate the Community Guidelines and/or Terms of Service. Thanks for helping us to keep Habitica safe and pleasant for everybody!
p.small.muted by Alys, Blade, and Matteo
tr
td
h5 Winter Plot-Line Continues
p SabreCat's news is dire. "Most of my sabertooth friends have been impossible to reach, but one thing is clear: the prides have been disappearing from the Steppes. There are also reports that something drove the mammoths to early migration and disturbed the hibernation of the terrible lizards."
p He wraps his cloak around himself as another blast of frigid wind roars through the streets. An icy winter gale has been blowing from the north, rattling the window panes and setting the pets and mounts to trembling and howling.
p "I've never seen anything like it!" says Matt the Beast Master. "Something is terrifying all my animals - even the cacti, who are normally so mighty and brave! For something to frighten a cactus..." He shakes his head.
p The stress level in Habitica is mounting.
p.small.muted Missed the previous Winter Plot-line? Catch up on the story <a href='http://habitrpg.wikia.com/wiki/Winter_Wonderland#Winter_Mystery_Plot' target='_blank'>here</a>!
p.small.muted by Lemoness
h5 12/21/2014 - Winter Wonderland Begins, Winter Class Outfits, Wintery Hair Colors, and NPC Decorations!
tr
td
h5 Winter Wonderland Begins!
p Winter has arrived, and the snow is gently drifting down over Habit City. Come celebrate with us!
tr
td
h5 Winter Class Outfits
.promo_winterclasses2015.pull-right
p From now until January 31st, limited edition outfits are available in the Rewards column. Depending on your class, you can be a Soothing Skater, Mage of the North, Gingerbread Warrior, or Icicle Drake! You'd better get productive to earn enough gold before they disappear. Good luck!
p.small.muted by Lemoness
tr
td
h5 Wintery Hair Colors
.promo_winteryhair.pull-right
p The Seasonal Edition Wintery Hair Colors are now available for purchase in the avatar customizations page! Now you can dye your avatar's hair Holly Green, Winter Star, Snowy, Peppermint, Aurora, or Festive.
p Seasonal Edition items recur unchanged every year, but they are only available to purchase during a short period of time. This is different from Limited Edition Items, which only recur if something is changed, such as the art or the price. These hair colors may remind some of you of the Holiday Hair Colors that were available last winter. The Holiday Hair Colors have been Retired in favor of the similar Seasonal Edition Wintery Hair Colors. Read more about the difference between Seasonal and Limited Edition items <a href='http://habitrpg.wikia.com/wiki/Item_Availability'>here</a>!
p.small.muted by Lemoness, crystalphoenix, and mariahm
tr
td
h5 NPC Decorations
.npc_alex.pull-right
p Looks like the NPCs are really getting in to the cheery winter mood around the site. Who wouldn't? After all, there's plenty more to come!
p.small.muted by Lemoness
h5 12/17/2014 - Android App Update, Seasonal Shop, and Winter Plot-Line Continues
tr
td
h5 Android App Update: December Art and Buying Gems!
p The December backgrounds and penguin pet quest are now visible in the Android mobile app! Also, weve made it possible to buy gems directly from the app. Now you dont have to switch to the website to stock up!
p You can get the Android app <a href='https://play.google.com/store/apps/details?id=com.ocdevel.habitrpg&hl=en' target='_blank'>here</a>! We will announce when the iOS app is available as well.
p.small.muted by negue
tr
td
h5 Seasonal Shop Tab
.seasonalshop_closed.pull-right
p Looks like a new tab has appeared under Inventory - the <a href='https://habitrpg.com/#/options/inventory/seasonalshop'>Seasonal Shop</a>! It's still closed, but I've heard a rumor that it will open soon...
p.small.muted by SabreCat and Lemoness
tr
td
h5 Winter Plot-Line Continues
p Lemoness bursts into the Tavern, shaking icicles off her hat. "The Stoïkalm Steppes are completely abandoned!" she says, gulping the cup of tea that Daniel the Barkeep offers her. "No people milling about, no mounts and pets playing in the snow - and when I tried to fly closer, my dragon spooked and refused to land!"
p A cloaked figure in the corner steps into the fire light - SabreCat, a powerful adventurer from the north. "The Stoïkalm Steppes are the last home of many animals that have long since gone extinct elsewhere," he says. "The stoic Stoïkalmers would never flee their lands unless something was threatening their pets and mounts!"
p He turns to Lemoness. "I can speak the language of the northern beasts. I'll try to contact the roaming sabertooth prides to see if they know what happened." As he lopes off into the distance, a cold wind begins to blow.
p.muted Missed the first part of the Winter Plot-Line? Read it <a href='http://habitrpg.wikia.com/wiki/Whats_new'>here</a>.
h5 12/9/2014 - Penguin Pet Quest and Winter Plot-Line
tr
td
h5 Penguin Pet Quest
.quest_penguin.pull-right
p Habiticans wanted to go ice-skating, but instead, a giant penguin is freezing everything in sight! All we wanted was to go ice-skating... Can you get this penguin to chill out? If so, you'll be rewarded with some penguins of your own!
p.small.muted by Melynnrose, Breadstrings, Rattify, Painter de Cluster, Daniel the Bard, and Leephon
tr
td
h5 Winter Plot-Line
p Lemoness enters the Tavern with worrying news from the far north of Habitica. "Nobody's heard from the Stoïkalm Steppes for over a week," she says. "It's hard to imagine anything troubling the citizens there, since it's such a placid part of the continent... But just in case, maybe I should pay a visit." Sounds like a good plan to us!
h5 12/3/2014 - Gifting Subscriptions And Gems, New Subscription Benefits, Mysterious Time Travelers, Steampunk Item Sets, And Block Subscriptions! h5 12/3/2014 - Gifting Subscriptions And Gems, New Subscription Benefits, Mysterious Time Travelers, Steampunk Item Sets, And Block Subscriptions!
table.table.table-striped table.table.table-striped
tr tr

View file

@ -187,3 +187,9 @@ div(ng-if='::profile.achievements.costumeContest')
=env.t('costumeContestText') =env.t('costumeContestText')
hr hr
div(ng-if='::profile.achievements.nye')
.achievement.achievement-nye
h5=env.t('auldAcquaintance')
small
=env.t('auldAcquaintanceText', {cards: "{{::profile.achievements.nye}}"})
hr

View file

@ -103,14 +103,14 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.task-notes span.task-notes
span.glyphicon.glyphicon-comment span.glyphicon.glyphicon-comment
//left-hand size commands //left-hand size commands
.task-controls .task-controls.task-primary
a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})') a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})')
if canceler if canceler
span.reward-cost {{Content.spells.special.#{k}.value}}
span.shop_gold span.shop_gold
span.reward-cost {{Content.spells.special.#{k}.value}}
else else
span.shop_spell(class='shop_#{k}')
span.reward-cost {{user.items.special.#{k}}} span.reward-cost {{user.items.special.#{k}}}
span(class='shop_#{k}',style='display:inline-block;vertical-align:top;')
// main content // main content
p.task-text {{Content.spells.special.#{k}.text()}} p.task-text {{Content.spells.special.#{k}.text()}}

View file

@ -4,7 +4,8 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
// Due Date // Due Date
span(ng-if='task.type=="todo" && task.date') span(ng-if='task.type=="todo" && task.date')
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{moment(task.date).format('MM/DD')}} span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:user.preferences.dateFormat.substr(0,5)}}
// Streak // Streak
| &nbsp; | &nbsp;
span(ng-show='task.streak') {{task.streak}}&nbsp; span(ng-show='task.streak') {{task.streak}}&nbsp;
@ -82,7 +83,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
//-| {{task.text}} //-| {{task.text}}
div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing') div(ng-if='task.checklist && !$state.includes("options.social.challenges") && !task.collapseChecklist && !task._editing')
fieldset.option-group fieldset.option-group.task-checklist
label.checkbox(ng-repeat='item in task.checklist') label.checkbox(ng-repeat='item in task.checklist')
input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)') input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
markdown(ng-model='item.text',target='_blank') markdown(ng-model='item.text',target='_blank')
@ -122,25 +123,24 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
a(ng-click="unlink(task, 'remove-all')")=env.t('removeThem') a(ng-click="unlink(task, 'remove-all')")=env.t('removeThem')
// Checklists // Checklists
div(ng-if='!$state.includes("options.social.challenges")') .task-checklist-edit(ng-if='!$state.includes("options.social.challenges")')
button.btn.btn-default.btn-sm(ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)') ul
span.glyphicon.glyphicon-tasks li
| &nbsp; &nbsp; button(type='button', ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)')
=env.t('addChecklist') span.glyphicon.glyphicon-tasks
span=env.t('addChecklist')
form.checklist-form(ng-if='task.checklist') form.checklist-form(ng-if='task.checklist')
fieldset.option-group.task-checklist(ng-if='!$state.includes("options.social.challenges")') fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
legend.option-title legend.option-title
span.hint(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom')=env.t('checklist') span.hint(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom')=env.t('checklist')
ul(hrpg-sort-checklist).list-unstyled ul(hrpg-sort-checklist)
li(ng-repeat='item in task.checklist') li(ng-repeat='item in task.checklist')
a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$event,$index,true)') //input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
span.checklist-icons.glyphicon.glyphicon-resize-vertical()
| &nbsp;
input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
//-,ng-blur='saveTask(task,true)') //-,ng-blur='saveTask(task,true)')
input.inline-edit(type='text',ng-model='item.text',ui-keydown="{'8 46':'removeChecklistItem(task,$event,$index)'}",ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}") span.checklist-icon.glyphicon.glyphicon-resize-vertical()
input(type='text',ng-model='item.text',ui-keydown="{'8 46':'removeChecklistItem(task,$event,$index)'}",ui-keyup="{'13':'addChecklistItem(task,$event,$index)','38 40':'navigateChecklist(task,$index,$event)'}")
a(ng-click='removeChecklistItem(task,$event,$index,true)')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
form(ng-submit='saveTask(task,false,true)') form(ng-submit='saveTask(task,false,true)')
// text & notes // text & notes
@ -152,27 +152,34 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
textarea.option-content(rows='3', ng-model='task.notes', ng-model-options="{debounce: 1000}") textarea.option-content(rows='3', ng-model='task.notes', ng-model-options="{debounce: 1000}")
// if Habit, plus/minus command options // if Habit, plus/minus command options
fieldset.option-group(ng-if='task.type=="habit" && !task.challenge.id') fieldset.option-group.plusminus(ng-if='task.type=="habit" && !task.challenge.id')
legend.option-title=env.t('direction/Actions') legend.option-title=env.t('direction/Actions')
span.task-checker.action-plusminus.select-toggle span.task-checker
input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-plus', type='checkbox', ng-model='task.up') input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-plus', type='checkbox', ng-model='task.up')
label(for='{{obj._id}}_{{task.id}}-option-plus') label(for='{{obj._id}}_{{task.id}}-option-plus')
span.task-checker.action-plusminus.select-toggle span.task-checker
input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-minus', type='checkbox', ng-model='task.down') input.visuallyhidden.focusable(id='{{obj._id}}_{{task.id}}-option-minus', type='checkbox', ng-model='task.down')
label(for='{{obj._id}}_{{task.id}}-option-minus') label(for='{{obj._id}}_{{task.id}}-option-minus')
// if Daily, calendar // if Daily, calendar
fieldset(ng-if='::task.type=="daily"', class="option-group") fieldset(ng-if='::task.type=="daily"', class="option-group")
legend.option-title=env.t('repeat') legend.option-title=env.t('repeat')
.task-controls.tile-group.repeat-days(bindonce) ul.repeat-days(bindonce)
// note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding
button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}} li
button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}} button(ng-class='{active: task.repeat.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}}
button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}} li
button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}} button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}}
button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}} li
button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}} button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}}
button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}} li
button(ng-class='{active: task.repeat.w}', type='button', ng-click='task.challenge.id || (task.repeat.w = !task.repeat.w)') {{::moment.weekdaysMin(3)}}
li
button(ng-class='{active: task.repeat.th}', type='button', ng-click='task.challenge.id || (task.repeat.th = !task.repeat.th)') {{::moment.weekdaysMin(4)}}
li
button(ng-class='{active: task.repeat.f}', type='button', ng-click='task.challenge.id || (task.repeat.f= !task.repeat.f)') {{::moment.weekdaysMin(5)}}
li
button(ng-class='{active: task.repeat.s}', type='button', ng-click='task.challenge.id || (task.repeat.s = !task.repeat.s)') {{::moment.weekdaysMin(6)}}
// if Reward, pricing // if Reward, pricing
fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id')
@ -184,7 +191,7 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
// if Todos, the due date // if Todos, the due date
fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id')
legend.option-title=env.t('dueDate') legend.option-title=env.t('dueDate')
input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true') input.option-content.datepicker(type='text', datepicker-popup='{{user.preferences.dateFormat}}', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true')
// Tags // Tags
fieldset.option-group(ng-if='!$state.includes("options.social.challenges")') fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
@ -199,10 +206,13 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}") fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}")
legend.option-title legend.option-title
a.hint.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty') a.hint.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title=env.t('difficultyHelpTitle'), popover-trigger='mouseenter', popover=env.t('difficultyHelpContent'))=env.t('difficulty')
.task-controls.tile-group.priority-multiplier ul.priority-multiplier
button.task-action-btn.tile(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy') li
button.task-action-btn.tile(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)')=env.t('medium') button(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy')
button.task-action-btn.tile(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard') li
button(type='button', ng-class='{active: task.priority==1.5}', ng-click='task.challenge.id || (task.priority=1.5)')=env.t('medium')
li
button(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard')
//span(ng-if='task.type=="daily" && !task.challenge.id') //span(ng-if='task.type=="daily" && !task.challenge.id')
span(ng-if='task.type=="daily"') span(ng-if='task.type=="daily"')
legend.option-title.pull-left=env.t('restoreStreak') legend.option-title.pull-left=env.t('restoreStreak')
@ -210,12 +220,17 @@ li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s
div(ng-if='(user.preferences.allocationMode == "taskbased" && user.preferences.automaticAllocation) || $state.is("options.social.challenges")') div(ng-if='(user.preferences.allocationMode == "taskbased" && user.preferences.automaticAllocation) || $state.is("options.social.challenges")')
legend.option-title.pull-left=env.t('attributes') legend.option-title.pull-left=env.t('attributes')
.task-controls.tile-group.task-attributes ul.task-attributes
button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical') li
button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="int"}', ng-click='task.attribute="int"')=env.t('mental') button(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical')
button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social') li
button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="per"}', ng-click='task.attribute="per"', popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top')=env.t('other') button(type='button', ng-class='{active: task.attribute=="int"}', ng-click='task.attribute="int"')=env.t('mental')
li
button(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social')
li
button(type='button', ng-class='{active: task.attribute=="per"}', ng-click='task.attribute="per"', popover=env.t('otherExamples'), popover-trigger='mouseenter', popover-placement='top')=env.t('other')
button.task-action-btn.tile.spacious(type='submit')=env.t('saveAndClose') .save-close
button(type='submit')=env.t('saveAndClose')
div(class='{{obj._id}}{{task.id}}-chart', ng-show='charts[obj._id+task.id]') div(class='{{obj._id}}{{task.id}}-chart', ng-show='charts[obj._id+task.id]')