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_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
"FLAG_REPORT_EMAIL": "email@mod.com",
"EMAIL_SERVER": {
"url": "http://example.com",
"authUser": "user",
@ -33,13 +34,22 @@
},
"PAYPAL":{
"billing_plans": {
"1":"1",
"3":"3",
"6":"6",
"12":"12"
"basic_earned":"basic_earned",
"basic_3mo":"basic_3mo",
"basic_6mo":"basic_6mo",
"google_6mo":"google_6mo",
"basic_12mo":"basic_12mo"
},
"mode":"sandbox",
"client_id":"client_id",
"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 = {
$push: {
'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",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
"in-app-purchase": "^0.2.0",
"jade": "~1.7.0",
"js2xmlparser": "~0.1.2",
"lodash": "~2.4.1",
"loggly": "~1.0.8",
"method-override": "~2.2.0",
"moment": "~2.8.3",
"mongoose": "~3.8.17",

View file

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

View file

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

View file

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

View file

@ -32,4 +32,33 @@
visibility: hidden
.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)
#wrap
margin-top: 0
padding: 0 4px;
overflow-x: hidden;
/* Gems
-------------------------------------------------- */
@ -135,6 +137,9 @@ a
a span.glyphicon
color: #333 // Takes base color from bootstrap
&.text-danger
color: #a94442 // Allows glyphicon to use text-danger class
a.label
color: #fff
@ -176,3 +181,5 @@ a.label
.white, .white a
color: #fff !important
.line-through
text-decoration line-through

View file

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

View file

@ -1,3 +1,7 @@
.quest_collected_true
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
padding-right: 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 $stage in $stages
.color-{$stage[0]}
background-color: $stage[1]
.action-yesno label,
.task-action-btn
background-color: darken($stage[1], 30%)
&:hover, &:focus
background-color: darken($stage[1], 40%)
.task-options
.task-column:not(.rewards)
.color-{$stage[0]}:not(.completed)
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
color: darken($completed,30%)
background-color: $completed
color: #999
.glyphicon
color: #333
.task-checker label
background-color: darken($completed, 30%)
border: 1px solid shade($completed,10%)
.priority-multiplier, .task-attributes, .repeat-days
li
hrpg-button-color-mixin($completed)
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
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
border: 1px solid $completed !important
// 2. Columns & Tasks
// ===================
@ -70,7 +140,7 @@ for $stage in $stages
// "Add new task" form
// -----------------
.task-add
margin-top: 1.618em
margin-top: 1.5em
@extend $hrpg-button-with-input
hrpg-button-color-mixin($color-options-submenu)
input, button
@ -90,7 +160,7 @@ for $stage in $stages
padding: 0
min-height: 3em
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
&:hover
cursor: move
@ -99,6 +169,16 @@ for $stage in $stages
label
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-text
@ -107,7 +187,7 @@ for $stage in $stages
line-height: 1.4
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
span.emoji
@ -118,6 +198,8 @@ for $stage in $stages
.habit-wide .task-text
padding-left: 7em
.habit-narrow .task-text
padding-left: 0.75em
// when a task is being dragged
.task.ui-sortable-helper
@ -174,28 +256,21 @@ for $stage in $stages
cursor: pointer
// plus/minus checkbox
.action-plusminus
label
margin-right: 1.5em
label:after
font-size: 1.41em
width: 2.12765em
height: 2.12765em
line-height: 2.12765
text-align: center
display: inline-block
vertical-align top
opacity: 0.3
background-color: #ddd
outline: 1px solid rgba(0,0,0,0.5)
color: #222
label[for$="plus"]:after
content: ''
label[for$="minus"]:after
content: ''
&.select-toggle input[type=checkbox]:checked + label:after
opacity: 1
.plusminus
text-align: center
.task-checker
font-size: 1.5em // 1.41em
@extend $hrpg-button
margin-bottom: 0.5em
&:first-of-type
margin-right: 0.5em
label:after
width: 2em
line-height: 1.5
label[for$="plus"]:after
content: ''
label[for$="minus"]:after
content: ''
// yes/no checkbox
.action-yesno
@ -227,9 +302,9 @@ for $stage in $stages
label:hover:after,
label:focus:after
content: "\E013"
font-family 'Glyphicons Halflings'
border none
margin 0
font-family: "Glyphicons Halflings"
border: none
margin: 0
line-height: 1.714285714em
height: 1.714285714em
width: 1.714285714em
@ -241,9 +316,9 @@ for $stage in $stages
// completed icon
input[type=checkbox]:checked + label:after
content: "\E013"
font-family 'Glyphicons Halflings'
border none
margin 0
font-family: "Glyphicons Halflings"
border: none
margin: 0
line-height: 1.714285714em
height: 1.714285714em
width: 1.714285714em
@ -295,36 +370,36 @@ form
//background-color:
.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
margin-bottom: 1em
.option-title
font-size: 1em
margin: 0 0 0.5em
line-height: 1.5
line-height: 2.5
border: 0
padding: 0
color: #333
font-style: italic
&:not(.mega):after
content: ':'
//color: #333
//font-style: italic
font-weight: bold
text-align: center
&.mega
cursor: pointer
font-style: normal
font-weight: bold
text-decoration: underline
//font-style: normal
&:after
font-family: "Glyphicons Halflings"
font-size: 0.75em
content: "\E114"
padding-left: 0.75em
//&:not(.mega):after
//content: ':'
.option-content
height: 2.5em
width: 100%
margin: 0 0 1em
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
height: 5em
@ -393,47 +468,66 @@ form
.task-options
.task-action-btn.tile
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
// --------
// Checklists
// --------
.checklist-form
/* Make an input blend into its parent */
input.inline-edit
reset-box-model()
reset-font()
line-height: inherit
text-align: inherit
background-color:inherit
box-shadow: none
width: 80%
.task-checklist-edit
> ul
text-align: center
li
@extend $hrpg-button
button
span
margin-right: 0.5em
> form
li
@extend $hrpg-button-with-input
@extend $clearfix
input
width: 70%
/* Add interaction cues on hover and focus */
input.inline-edit:hover,input.inline-edit:focus
background-color: #FFD
transition: background-color 0.5s
/* Add interaction cues on hover and focus */
&:hover,&:focus
background-color: #FFD
transition: background-color 0.5s
.checklist-icons
opacity:0
width: 5%
li
min-height: 20px
padding-left: 10px
margin-bottom: 5px
[type="checkbox"]
margin: 0px 10px 3px 0px
width: 4%
li:hover .checklist-icons
opacity:1
.checklist-icon
opacity:0.25
text-align: center
line-height: 1.5
float: left
padding: 0.25em 0.5em
li
margin-bottom: 1em
&:hover .checklist-icon
opacity:1
// Task filters
// --------
.task-filter
margin: 1.618em 0 1em 0
margin: 1.5em 0 1em 0
hrpg-button-bar-mixin($color-tasks)
@extend $hrpg-button-bar
width: 100%
@ -449,16 +543,18 @@ form
width: 100%
height: 100%
.rewards
.task
min-height: 3.07em // Temp fix for slight misalignment of btn-buy
.task-filter
li:nth-child(3n + 1):nth-last-child(2)
width: 33.333%
li:nth-child(3n + 1):nth-last-child(2) + li
width: 66.666%
.btn-buy
word-wrap: break-word
padding-left: 0.2em
text-align: left
border:none
span
position: relative
left: -2px
@ -474,9 +570,4 @@ form
margin: auto
.shop_gold + .reward-cost
line-height: 1
top: -0.1em
.repeat-days
a
border-radius: 0.382em
top: -0.1em

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
// temporary hack until they have a better solution
.value('$anchorScroll', angular.noop)
.constant("API_URL", "")
.constant("STORAGE_USER_ID", 'habitrpg-user')
@ -185,6 +184,10 @@ window.habitrpg = angular.module('habitrpg',
url: '/timetravelers',
templateUrl: "partials/options.inventory.timetravelers.html"
})
.state('options.inventory.seasonalshop', {
url: '/seasonalshop',
templateUrl: "partials/options.inventory.seasonalshop.html"
})
// 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>
var addthisServices = 'facebook,twitter,googleplus,tumblr,'+window.env.BASE_URL.replace('https://','').replace('http://','');
window.addthis_config = {
ui_click: true,
services_custom:{
name: "Download",
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',
function($scope, $rootScope, Members, Shared, $http, Notification) {
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups',
function($scope, $rootScope, Members, Shared, $http, Notification, Groups) {
$scope.timestamp = function(timestamp){
return moment(timestamp).format('MM/DD/YYYY');
}
@ -129,7 +129,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.gift = {
type: 'gems',
gems: {amount:0, fromBalance:true},
subscription: {months:0},
subscription: {key:''},
message:''
};
$scope.sendGift = function(uuid, gift){
@ -139,6 +139,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$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._sending = false;
@ -239,12 +253,14 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.deleteChatMessage = function(group, message){
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;
Groups.Group.deleteChatMessage({gid:group._id, messageId:message.id, previousMsg:previousMsg}, undefined, function(data){
if(data.chat) group.chat = data.chat;
if(confirm('Are you sure you want to delete this message?')){
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});
if(i !== -1) group.chat.splice(i, 1);
});
var i = _.findIndex(group.chat, {id: message.id});
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');
}
$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){
group.$get();
}

View file

@ -88,16 +88,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
}
$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;
if(gems < item.value) return $rootScope.openModal('buyGems');
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
var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
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
if (type == 'weapon' || type == 'armor' || type == 'head' || type == 'shield') {
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){
@ -157,5 +164,23 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
$rootScope.selectedQuest = item;
$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) {
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;
$scope.spell = spell;
if (spell.target == 'self') {
@ -236,7 +239,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$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.
$rootScope.hardRedirect = function(url){
window.location.href = url;

View file

@ -65,6 +65,8 @@ habitrpg.controller('SettingsCtrl',
User.set({'preferences.language': $scope.language.code});
}
$scope.availableFormats = ['MM/dd/yyyy','dd/MM/yyyy', 'yyyy/MM/dd'];
$scope.reroll = function(){
User.user.ops.reroll({});
$rootScope.$state.go('tasks');
@ -179,5 +181,15 @@ habitrpg.controller('SettingsCtrl',
$scope.deleteWebhook = function(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) {
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) {

View file

@ -13,6 +13,8 @@ angular.module('groupServices', ['ngResource']).
//query: {method: "GET", isArray:false},
postChat: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat'},
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'},
leave: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/leave'},
invite: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/invite'},

View file

@ -3,23 +3,12 @@
angular.module('paymentServices',[]).factory('Payments',
['$rootScope', 'User', '$http', 'Content',
function($rootScope, User, $http, Content) {
var user = User.user;
var plan = User.user.purchased.plan;
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) {
var sub =
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;
sub = sub && Content.subscriptionBlocks[sub];
var amount = // 500 = $5
@ -36,7 +25,8 @@ function($rootScope, User, $http, Content) {
token: function(res) {
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.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() {
window.location.reload(true);
}).error(function(res) {
@ -50,9 +40,9 @@ function($rootScope, User, $http, Content) {
StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY,
address: false,
name: 'Update',
description: 'Update the card to be charged for your subscription',
panelLabel: 'Update Card',
name: window.env.t('subUpdateTitle'),
description: window.env.t('subUpdateDescription'),
panelLabel: window.env.t('subUpdateCard'),
token: function(data) {
var url = '/stripe/subscribe/edit';
$http.post(url, data).success(function() {
@ -66,7 +56,7 @@ function($rootScope, User, $http, Content) {
Payments.cancelSubscription = function(){
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){

View file

@ -148,21 +148,19 @@ api.loginSocial = function(req, res, next) {
network = req.body.network;
if (network!=='facebook')
return res.json(401, {err:"Only Facebook supported currently."});
async.waterfall([
function(cb){
async.auto({
profile: function (cb) {
passport._strategies[network].userProfile(access_token, cb);
},
function(profile, cb) {
var q = {};q['auth.'+network+'.id'] = profile.id;
User.findOne(q, {_id:1, apiToken:1, auth:1}, function(err, user){
if (err) return cb(err);
cb(null, {user:user, profile:profile});
});
},
function(data, cb){
if (data.user) return cb(null, data.user);
user: ['profile', function (cb, results) {
var q = {};
q['auth.' + network + '.id'] = results.profile.id;
User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
}],
register: ['profile', 'user', function (cb, results) {
if (results.user) return cb(null, results.user);
// Create new user
var prof = data.profile;
var prof = results.profile;
var user = {
preferences: {
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.save(cb);
if(isProd && prof.emails && prof.emails[0] && prof.emails[0].value){
utils.txnEmail({name:prof.displayName || prof.username, email:prof.emails[0].value}, 'welcome');
if (isProd && prof.emails && prof.emails[0] && prof.emails[0].value) {
utils.txnEmail({name: prof.displayName || prof.username, email: prof.emails[0].value}, 'welcome');
}
ga.event('register', network).send();
}
], function(err, user){
}]
}, function(err, results){
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
return res.json(200, {id: user.id, token:user.apiToken});
var acct = results.register[0] ? results.register[0] : results.register;
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});
user.auth.local.salt = salt;
user.auth.local.hashed_password = hashed_password;
utils.sendEmail({
from: "HabitRPG <admin@habitrpg.com>",
to: email,
subject: "Password Reset for HabitRPG",
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL'),
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong>. Log in at " + nconf.get('BASE_URL')
utils.txnEmail(user, 'reset-password', [
{name: "NEW_PASSWORD", content: newPassword},
{name: "USERNAME", content: user.auth.local.username}
]);
user.save(function(err){
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
api.create = function(req, res, next){
var user = res.locals.user;
var group, chal;
// First, make sure they've selected a legit group, and store it for later
var waterfall = [
function(cb){
Group.findById(req.body.group).exec(cb);
async.auto({
get_group: function(cb){
var q = {_id:req.body.group};
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){
if (!_group) return cb("Group." + req.body.group + " not found");
group = _group;
cb(null);
}
];
save_chal: ['get_group', function(cb, results){
var group = results.get_group,
prize = +req.body.prize;
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 (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'});
if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'});
if (+req.body.prize > 0) {
waterfall.push(function(cb){
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
var prizeCost = req.body.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 (groupBalance >= 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`
if (groupBalance >= 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;
}
}
req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official;
var chal = new Challenge(req.body); // FIXME sanitize
chal.members.push(user._id);
chal.save(cb)
},
function(_chal, num, cb){
chal = _chal;
group.challenges.push(chal._id);
group.save(cb);
},
function(_group, num, cb) {
chal.save(cb);
}],
save_group: ['save_chal', function(cb, results){
results.get_group.challenges.push(results.save_chal[0]._id);
results.get_group.save(cb);
}],
sync_user: ['save_group', function(cb, results){
// Auto-join creator to challenge (see members.push above)
chal.syncToUser(user, cb);
}
]);
async.waterfall(waterfall, function(err){
if (err) return next(err);
res.json(chal);
user = group = chal = null;
});
results.save_chal[0].syncToUser(user, cb);
}]
}, function(err, results){
if (err) return err.code? res.json(err.code, err) : next(err);
return res.json(results.save_chal[0]);
user = null;
})
}
// UPDATE

View file

@ -7,10 +7,12 @@ function clone(a) {
var _ = require('lodash');
var nconf = require('nconf');
var async = require('async');
var utils = require('./../utils');
var shared = require('habitrpg-shared');
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var isProd = nconf.get('NODE_ENV') === 'production';
var api = module.exports;
/*
@ -187,7 +189,7 @@ api.update = function(req, res, next) {
if(group.leader !== user._id)
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];
});
@ -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){
// 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

View file

@ -35,7 +35,7 @@ api.sendMessage = function(user, member, data){
msg = data.message
} else {
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;
}
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 stripe = require('./stripe');
var paypal = require('./paypal');
var User = require('mongoose').model('User');
var members = require('../members')
var async = require('async');
var iap = require('./iap');
var mongoose= require('mongoose');
var cc = require('coupon-code');
function revealMysteryItems(user) {
_.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;
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case
var p = recipient.purchased.plan;
var months = +(data.gift ? data.gift.subscription.months : data.sub.months);
var block = shared.content.subscriptionBlocks[months];
var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
var months = +block.months;
if (data.gift) {
if (p.customerId && !p.dateTerminated) { // User has active plan
@ -114,6 +116,14 @@ exports.buyGems = function(data, 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.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit;
@ -123,4 +133,7 @@ exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment;
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 paypal = require('paypal-rest-sdk');
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
// 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
_.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({
@ -30,23 +32,33 @@ var parseErr = function(res, err){
}
exports.createBillingAgreement = function(req,res,next){
req.session.paypalBlock = req.query.sub;
var block = shared.content.subscriptionBlocks[req.query.sub];
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+block.price+' every '+block.months+' months, recurring)';
var billingAgreementAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"start_date": moment().add({minutes:5}).format(),
"plan": {
"id": block.paypalKey
var sub = shared.content.subscriptionBlocks[req.query.sub];
async.waterfall([
function(cb){
if (!sub.discount) return cb(null, null);
if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
},
"payer": {
"payment_method": "paypal"
function(coupon, cb){
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);
}
};
paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) {
], function(err, billingAgreement){
if (err) return parseErr(res, err);
// 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;
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 price = !gift ? 5.00
: 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"
: 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 = {
"intent": "sale",
"payer": {

View file

@ -11,7 +11,7 @@ var paypal = require('paypal-rest-sdk');
var blocks = require('habitrpg-shared').content.subscriptionBlocks;
var live = nconf.get('PAYPAL:mode')=='live';
var OP = 'get'; // list create update remove
var OP = 'create'; // list create update remove
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
@ -72,7 +72,7 @@ switch(OP) {
});
break;
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 (plan.state == "ACTIVE")
return console.log({err:err, plan:plan});

View file

@ -4,6 +4,8 @@ var async = require('async');
var payments = require('./index');
var User = require('mongoose').model('User');
var shared = require('habitrpg-shared');
var mongoose = require('mongoose');
var cc = require('coupon-code');
/*
Setup Stripe response when posting payment
@ -17,16 +19,27 @@ exports.checkout = function(req, res, next) {
async.waterfall([
function(cb){
if (sub) {
stripe.customers.create({
email: req.body.email,
metadata: {uuid: user._id},
card: token,
plan: sub.key
}, cb);
async.waterfall([
function(cb2){
if (!sub.discount) return cb2(null, null);
if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
},
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 {
stripe.charges.create({
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,
currency: "usd",
card: token

View file

@ -257,37 +257,57 @@ api.update = function(req, res, next) {
};
api.cron = function(req, res, next) {
var user = res.locals.user,
progress = user.fns.cron(),
ranCron = user.isModified(),
quest = shared.content.quests[user.party.quest.key];
try{
var user = res.locals.user,
progress = user.fns.cron(),
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;
if (!ranCron) return next(null,user);
Group.tavernBoss(user,progress);
if (!quest) return user.save(next);
// FOR DEBUGGING, PLEASE IGNORE
var opStatus = null;
// 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?
async.waterfall([
function(cb){
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
res.locals.user = saved;
next(err,saved);
user = progress = quest = null;
});
// 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?
async.waterfall([
function(cb){
opStatus = 'saveUser';
user.save(cb); // make sure to save the cron effects
},
function(saved, count, cb){
opStatus = 'runQuest';
var type = quest.boss ? 'boss' : 'collect';
Group[type+'Quest'](user,progress,cb);
},
function(){
var cb = arguments[arguments.length-1];
// User has been updated in boss-grapple, reload
User.findById(user._id, cb);
}
], function(err, saved) {
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-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) {
logger = new (winston.Logger)({});
@ -49,3 +65,8 @@ module.exports.error = function(/* variable args */) {
if (logger)
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) +
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
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;
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);
@ -173,12 +180,9 @@ module.exports.locals = function(req, res, next) {
language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
var tavern = require('./models/group').tavern;
res.locals.habitrpg = {
NODE_ENV: nconf.get('NODE_ENV'),
BASE_URL: nconf.get('BASE_URL'),
GA_ID: nconf.get("GA_ID"),
var envVars = _.pick(nconf.get(), 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY'.split(' '));
res.locals.habitrpg = _.merge(envVars, {
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
getManifestFiles: getManifestFiles,
getBuildUrl: getBuildUrl,
avalaibleLanguages: i18n.avalaibleLanguages,
@ -193,11 +197,9 @@ module.exports.locals = function(req, res, next) {
siteVersion: siteVersion,
Content: shared.content,
mods: require('./models/user').mods,
FACEBOOK_KEY: nconf.get('FACEBOOK_KEY'),
tavern: tavern, // for world boss
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
};
});
// Put query-string party invitations into session to be handled later
try{

View file

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

View file

@ -26,7 +26,10 @@ var GroupSchema = new Schema({
# 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},
challengeCount: {type: Number, 'default': 0},
balance: Number,
@ -98,7 +101,9 @@ var chatDefaults = module.exports.chatDefaults = function(msg,user){
id: shared.uuid(),
text: msg,
timestamp: +new Date,
likes: {}
likes: {},
flags: {},
flagCount: 0
};
if (user) {
_.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;
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
@ -330,4 +347,4 @@ Group.count({_id:'habitrpg'},function(err,ct){
type: 'guild',
privacy:'public'
}).save();
})
})

View file

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

View file

@ -577,11 +577,37 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
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/{uuid}":
spec:{}
"/members/{uuid}:GET":
spec:
path: '/members/{uuid}'
description: "Get a member."
parameters: [path('uuid','Member ID','string')]
middleware: [auth.auth, i18n.getUserLanguage]
action: members.getMember
"/members/{uuid}/message":
spec:
@ -589,7 +615,7 @@ module.exports = (swagger, v2) ->
description: 'Send a private message to a member'
parameters: [
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]
action: members.sendPrivateMessage
@ -608,7 +634,7 @@ module.exports = (swagger, v2) ->
description: 'Send a gift to a member'
parameters: [
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]
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/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;

View file

@ -35,6 +35,8 @@ function getMailingInfo(user) {
}
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.email) return;
request({

View file

@ -141,7 +141,7 @@ describe "API", ->
id: res.body.todos[0].id
body:
dateCompleted: moment().subtract("days", 4)
dateCompleted: moment().subtract(4, "days")
}
{
op: "updateTask"
@ -149,7 +149,7 @@ describe "API", ->
id: res.body.todos[1].id
body:
dateCompleted: moment().subtract("days", 4)
dateCompleted: moment().subtract(4, "days")
}
]).end (res) ->
expect(_.size(res.body.todos)).to.be 4
@ -460,7 +460,7 @@ describe "API", ->
{
op: "update"
body:
lastCron: moment().subtract("days", 1)
lastCron: moment().subtract(1, "days")
}
]).end (res) ->
expect(res.body.party.quest.progress.up).to.be.above up
@ -532,7 +532,7 @@ describe "API", ->
{
op: "update"
body:
lastCron: moment().subtract("days", 1)
lastCron: moment().subtract(1, "days")
}
]).end ->
cb()
@ -608,7 +608,7 @@ describe "API", ->
it "Handles unsubscription", (done) ->
cron = ->
user.lastCron = moment().subtract("d", 1)
user.lastCron = moment().subtract(1, "d")
user.fns.cron()
expect(user.purchased.plan.customerId).to.not.be.ok()
@ -624,7 +624,7 @@ describe "API", ->
cron()
expect(user.purchased.plan.customerId).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()
expect(user.purchased.plan.customerId).to.not.be.ok()
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') }")
a(ui-sref='options.inventory.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')
| 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-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]')
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')
.container-fluid
.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
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)')
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}}
@ -98,12 +134,10 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
+specialItem('snowball')
+specialItem('spookDust')
//-menu.pets-menu(label=env.t('valentineCard'))
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')
.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')
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}}
@ -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')")
.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
h2=env.t('market')
.row-fluid
@ -178,15 +216,6 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p
| 4
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')
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')
@ -197,7 +226,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p
| 4
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"}})')
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}}

View file

@ -15,8 +15,9 @@ mixin customizeProfile(mobile)
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')
menu(label=env.t(title))
+gemCost(2)
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"/>'
span(ng-hide='#{status=="disabled"} || #{showPath("user.purchased."+path, colors, "&&")}')
+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
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'))
- var specialShirts = ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie']
+gemCost(2)
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"/>'
span(ng-hide='#{showPath("user.purchased.shirt", specialShirts, "&&")}')
+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
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}")')
+buyPref('hair.color', ['rainbow','yellow','green','purple','blue','TRUred'], 'rainbowColors')
+buyPref('hair.color', ['candycorn','ghostwhite','halloween','midnight','pumpkin','zombie'], 'hauntedColors', 'disabled')
+buyPref('hair.color', ['aurora','festive','hollygreen','peppermint','snowy','winterstar'], 'winteryColors')
li.customize-menu
menu(label=env.t('bodyHair'))
@ -80,8 +83,9 @@ mixin customizeProfile(mobile)
// Purchasable hairstyles
menu(label=env.t('hairSet1'))
- var colors = [2,4,5,6,7,8]
+gemCost(2)
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"/>'
span(ng-hide='#{showPath("user.purchased.hair.base", colors, "&&")}')
+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
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}})')
li.customize-menu
h5=env.t('bodyFacialHair')
+gemCost(2)
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"/>'
menu(label=env.t('bodyFacialHair'))
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"]')
+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
menu(label=env.t('beard'))
@ -249,9 +254,9 @@ mixin backgrounds(mobile)
- var k = bgsKeys[i], bgs = env.Content.backgrounds[k];
li.customize-menu
menu(label=env.t(k))
+gemCost(7)
//-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"/>'
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"/>'
span(ng-hide="ownsSet('background',#{JSON.stringify(bgs)})")
+gemCost(7)
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
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}")

View file

@ -36,6 +36,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
br
strong
!=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
label
input(type='checkbox', ng-click='hideHeader() ', ng-checked='user.preferences.hideHeader!==true')
@ -200,7 +204,7 @@ mixin subPerks()
tr
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.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
td
span.hint(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory')
@ -210,8 +214,8 @@ mixin subPerks()
tr
td
span.hint(popover=env.t('mysteryItemText'),popover-trigger='mouseenter',popover-placement='right') #{env.t('mysteryItem')}&nbsp;
div(ng-show='_subscription.months>1')
.badge.badge-success +{{Math.floor(_subscription.months/3)}} Mystic Hourglass
div(ng-show='_subscription.key!="basic_earned"')
.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.
tr
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')
//-h2=env.t('individualSub')
.container-fluid(ng-init='_subscription={months:1}')
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
.row
.col-md-6
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>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
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
span.glyphicon.glyphicon-credit-card
| 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}}
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
.form-group
each block in env.Content.subscriptionBlocks
.radio
label
input(type="radio", name="subRadio", value="#{block.months}", ng-model='_subscription.months')
//-| #{block.months} Month(s) Recurring: $#{block.price} #{env.t('monthUSD')}
| Recurring $#{block.price} each #{block.months} Month(s)
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
span(ng-show='block.original')
| Recurring <span class='line-through'>${{block.original}}</span> <span class='label label-success'>${{block.price}}</span> 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
a.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.months})', ng-disabled='!_subscription.months') 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-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.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key') PayPal
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')

View file

@ -1,17 +1,20 @@
mixin chatMessages(inbox)
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'
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" : ""}}')
.scrollable-message
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")
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')
a.label.label-default.chat-message.hidden-label
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
| &nbsp; &nbsp;
span {{::message.user}}&nbsp;
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
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
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)}}
@ -22,11 +25,15 @@ mixin chatMessages(inbox)
if inbox
a(ng-click="quickReply(message.uuid)")
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"}')
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)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
| &nbsp;
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}&nbsp;
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')
p
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"')
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
label=env.t('logoUrl')
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')
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
label
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')
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')
=env.t('party')
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')
li(ng-class="{ active: $state.includes('options.social.challenges') }")
a(ui-sref='options.social.challenges')
=env.t('challenges')
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')
.tab-content

View file

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

View file

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

View file

@ -27,4 +27,30 @@ script(id='modals/valentine.html', type='text/ng-template')
p
small For enduring such a saccharine poem, you both receive the "Adoring Friends" badge!
.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;
=env.t('memberSince')
|&nbsp;
| {{::timestamp(profile.auth.timestamps.created)}} -
| {{::profile.auth.timestamps.created | date:user.preferences.dateFormat}} -
li(ng-show='::profile.auth.timestamps.loggedin')
|&nbsp;
=env.t('lastLoggedIn')
|&nbsp;
| {{::timestamp(profile.auth.timestamps.loggedin)}} -
| {{::profile.auth.timestamps.loggedin | date:user.preferences.dateFormat}} -
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] }}
include ../profiles/stats
@ -46,7 +46,7 @@ script(type='text/ng-template', id='modals/private-message.html')
.modal-header
h4=env.t('pmHeading', {name: "{{profile.profile.name}}"})
.modal-body
textarea.form-control(type='text',ng-model='_message')
textarea.form-control(type='text',rows='5',ng-model='_message')
.modal-footer
button.btn.btn-primary(ng-click='sendPrivateMessage(profile._id, _message)')=env.t("send")
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-body
.form-group
each block in env.Content.subscriptionBlocks
.radio
label
input(type="radio", name="subRadio", value="#{block.months}", ng-model='gift.subscription.months')
| #{block.months} Month(s): $#{block.price}
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit:"discount==true" | orderBy:"months"')
label
input(type="radio", name="subRadio", ng-value="block.key", ng-model='gift.subscription.key')
| {{block.months}} Month(s): ${{block.price}}
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")
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
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()}}"
=env.t('completed')
.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()')
quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived'))
.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
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
h5 Party Hats
.promo_partyhats.pull-right
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 Lemoness and SabreCat
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 New Year's Cards (Until Jan 1st Only!)
.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
a(href='/static/old-news', target='_blank') Read older news
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!
table.table.table-striped
tr

View file

@ -187,3 +187,9 @@ div(ng-if='::profile.achievements.costumeContest')
=env.t('costumeContestText')
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.glyphicon.glyphicon-comment
//left-hand size commands
.task-controls
.task-controls.task-primary
a.money.btn-buy.item-btn(ng-click='castStart(Content.spells.special.#{k})')
if canceler
span.reward-cost {{Content.spells.special.#{k}.value}}
span.shop_gold
span.reward-cost {{Content.spells.special.#{k}.value}}
else
span.shop_spell(class='shop_#{k}')
span.reward-cost {{user.items.special.#{k}}}
span(class='shop_#{k}',style='display:inline-block;vertical-align:top;')
// main content
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
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
| &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}}
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')
input(type='checkbox',ng-model='item.completed',ng-change='saveTask(task,true)')
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')
// Checklists
div(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)')
span.glyphicon.glyphicon-tasks
| &nbsp; &nbsp;
=env.t('addChecklist')
.task-checklist-edit(ng-if='!$state.includes("options.social.challenges")')
ul
li
button(type='button', ng-if='!task.checklist[0] && (task.type=="daily" || task.type=="todo")',ng-click='addChecklist(task)')
span.glyphicon.glyphicon-tasks
span=env.t('addChecklist')
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
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')
a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$event,$index,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)')
//input(type='checkbox',ng-model='item.completed',ng-change='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)')
// 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}")
// 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')
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')
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')
label(for='{{obj._id}}_{{task.id}}-option-minus')
// if Daily, calendar
fieldset(ng-if='::task.type=="daily"', class="option-group")
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
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)}}
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.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)}}
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.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)}}
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.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.su}', type='button', ng-click='task.challenge.id || (task.repeat.su = !task.repeat.su)') {{::moment.weekdaysMin(0)}}
li
button(ng-class='{active: task.repeat.m}', type='button', ng-click='task.challenge.id || (task.repeat.m = !task.repeat.m)') {{::moment.weekdaysMin(1)}}
li
button(ng-class='{active: task.repeat.t}', type='button', ng-click='task.challenge.id || (task.repeat.t = !task.repeat.t)') {{::moment.weekdaysMin(2)}}
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
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
fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id')
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
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}")
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')
.task-controls.tile-group.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')
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.task-action-btn.tile(type='button', ng-class='{active: task.priority==2}', ng-click='task.challenge.id || (task.priority=2)')=env.t('hard')
ul.priority-multiplier
li
button(type='button', ng-class='{active: task.priority==1 || !task.priority}', ng-click='task.challenge.id || (task.priority=1)')=env.t('easy')
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"')
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")')
legend.option-title.pull-left=env.t('attributes')
.task-controls.tile-group.task-attributes
button.task-action-btn.tile(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=="int"}', ng-click='task.attribute="int"')=env.t('mental')
button.task-action-btn.tile(type='button', ng-class='{active: task.attribute=="con"}', ng-click='task.attribute="con"')=env.t('social')
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')
ul.task-attributes
li
button(type='button', ng-class='{active: task.attribute=="str"}', ng-click='task.attribute="str"')=env.t('physical')
li
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]')