refactor(tasks) improve UI consistency

* Round corners for UI elements / “8-bit” outlines for RPG elements.
* Tag bar – Make it clearer that “Tags” is a heading, not an option
* Task filters – restyle in line with tag bar + nav menu
This commit is contained in:
benmanley 2014-11-27 10:32:57 +00:00
parent 16fff701d4
commit 50b1cba0a6
62 changed files with 1607 additions and 1005 deletions

2
.buildpacks Normal file
View file

@ -0,0 +1,2 @@
https://github.com/heroku/heroku-buildpack-nodejs.git
https://github.com/stomita/heroku-buildpack-phantomjs.git

View file

@ -16,16 +16,15 @@
"dependencies": {
"jquery": "~2.1.0",
"jquery.cookie": "~1.4.0",
"angular": "1.3.0-beta.11",
"angular": "1.3.3",
"angular-ui": "~0.4.0",
"angular-sanitize": "1.3.0-beta.11",
"angular-resource": "1.3.0-beta.11",
"angular-sanitize": "1.3.3",
"angular-resource": "1.3.3",
"angular-ui-utils": "~0.1.0",
"angular-ui-select2": "git://github.com/angular-ui/ui-select2.git",
"angular-bootstrap": "~0.10.0",
"angular-bootstrap": "~0.12.0",
"angular-ui-router": "git://github.com/HabitRPG/ui-router.git#habitrpg",
"angular-loading-bar": "~0.3.0",
"angular-bindonce": "~0.2.1",
"angular-loading-bar": "~0.6.0",
"bootstrap": "~3.1.0",
"bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master",
"bootstrap-tour": "~0.8.1",
@ -34,18 +33,22 @@
"github-buttons": "git://github.com/mdo/github-buttons.git",
"marked": "~0.2.9",
"Angular-At-Directive": "git://github.com/snicker/Angular-At-Directive#master",
"js-emoji": "git://github.com/snicker/js-emoji#master",
"js-emoji": "https://github.com/iamcal/js-emoji.git",
"sticky": "*",
"swagger-ui": "git://github.com/wordnik/swagger-ui.git",
"ngInfiniteScroll": "1.0.0",
"jquery-colorbox": "~1.4.36",
"pnotify": "~1.3.1",
"jquery-ui": "~1.10.3"
"jquery-ui": "~1.10.3",
"hello": "~1.3.1",
"css-social-buttons": "https://github.com/samcollins/css-social-buttons.git",
"angular-filter": "~0.5.1"
},
"devDependencies": {
"angular-mocks": "1.3.0-beta.11"
"angular-mocks": "1.3.3"
},
"resolutions": {
"angular": "1.3.0-beta.11"
"angular": "1.3.3",
"jquery": ">=1.9.0"
}
}

View file

@ -20,10 +20,20 @@
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY",
"GA_ID": "GA_ID",
"PAYPAL_USERNAME": "PAYPAL_USERNAME",
"PAYPAL_PASSWORD": "PAYPAL_PASSWORD",
"PAYPAL_SIGNATURE": "PAYPAL_SIGNATURE",
"EMAIL_SERVER_URL": "http://example.com",
"EMAIL_SERVER_AUTH_USER": "user",
"EMAIL_SERVER_AUTH_PASSWORD": "password"
"EMAIL_SERVER": {
"url": "http://example.com",
"authUser": "user",
"authPassword": "password"
},
"S3":{
"bucket":"bucket",
"accessKeyId":"accessKeyId",
"secretAccessKey":"secretAccessKey"
},
"PAYPAL":{
"billing_plan_id":"billing_plan_id",
"mode":"sandbox",
"client_id":"client_id",
"client_secret":"client_secret"
}
}

View file

@ -0,0 +1,11 @@
db.users.update(
{'items.pets.Turkey-Base':{$ne:null}},
{$set:{'items.mounts.Turkey-Base':true}},
{multi:1}
)
db.users.update(
{'items.pets.Turkey-Base':null},
{$set:{'items.pets.Turkey-Base':5}},
{multi:1}
)

View file

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

View file

@ -5,6 +5,7 @@
"main": "./src/server.js",
"dependencies": {
"async": "~0.9.0",
"aws-sdk": "^2.0.25",
"bower": "~1.3.12",
"coffee-script": "1.6.x",
"connect-ratelimit": "0.0.7",
@ -25,24 +26,26 @@
"grunt-nodemon": "~0.3.0",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"icalendar": "git://github.com/lefnire/node-icalendar#master",
"jade": "~1.7.0",
"js2xmlparser": "~0.1.2",
"lodash": "~2.4.1",
"jade": "~1.7.0",
"method-override": "~2.2.0",
"mongoose": "~3.8.17",
"moment": "~2.8.3",
"mongoose": "~3.8.17",
"mongoose-id-autoinc": "~2013.7.14-4",
"nconf": "~0.6.9",
"newrelic": "~1.11.2",
"nib": "~1.0.1",
"nodemailer": "~0.5.2",
"pageres": "^1.0.1",
"passport": "~0.2.1",
"passport-facebook": "~1.0.2",
"paypal-express-checkout": "git://github.com/HabitRPG/node-paypal-express-checkout#habitrpg",
"paypal-ipn": "~1.0.1",
"paypal-recurring": "git://github.com/jaybryant/paypal-recurring#656b496f43440893c984700191666a5c5c535dca",
"passport-facebook": "Fonger/passport-facebook#a8f98adcddad99caa9a918bc7b76462c92c5c9fd",
"paypal-ipn": "2.1.0",
"paypal-rest-sdk": "^1.2.1",
"pretty-data": "git://github.com/vkiryukhin/pretty-data#master",
"qs": "^2.3.2",
"request": "~2.44.0",
"s3-upload-stream": "^1.0.6",
"stripe": "*",
"swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg",
"universal-analytics": "~0.3.2",

View file

@ -9,9 +9,13 @@ Seeing as the sprites might change drastically in the
future re: pets and whatnot, this is just temporary.
---------------------------------------------------- */
.profile-modal-header
width:560px
margin: 0px auto
// see http://stackoverflow.com/questions/24166568/set-bootstrap-modal-body-height-by-percentage
.profile-modal
.modal-dialog,.modal-content
height: 96%
.modal-body
max-height: calc(100% - 150px) //100% = dialog height, 150px = header + footer
overflow-y: scroll
.herobox
// Base styles
@ -73,7 +77,17 @@ future re: pets and whatnot, this is just temporary.
&.isUser
background: $color-herobox // Set a different background color for the current user
&:hover, &:focus
background: lighten($color-herobox, 16.18%)
background: lighten($color-herobox, 16.18%)
.addthis_pill_style
width: 50px !important
.addthis_native_toolbox
position: absolute
top:2px
right:2px
opacity:0
&:hover .addthis_native_toolbox
opacity:1
//Need to find a way to indicate who is leader without using background-color as it won't work when a background image is applied
//.herobox.isLeader.noBackgroundImage

View file

@ -8,7 +8,7 @@
list-style: none
margin-right: 1em
.filters-controls
@extend $hrpg-button-bar
@extend $hrpg-button-bar-with-heading
hrpg-button-bar-mixin($color-options-menu)
li
@extend $hrpg-button-master
@ -18,11 +18,14 @@
.filters-edit
margin-bottom: 0.382em
.filters-tags
@extend $hrpg-tag-master
hrpg-tag-color-mixin($color-filter-tag)
@extend $hrpg-button
hrpg-button-color-mixin($color-filter-tag)
margin-bottom: 0.618em
margin-right: 0.618em
&.challenge
hrpg-tag-color-mixin($color-filter-tag-challenge)
hrpg-button-color-mixin($color-filter-tag-challenge)
span
margin-right: 0.382em
form
display: none
markdown

View file

@ -24,8 +24,8 @@ $color-button-style-one = $best
// Task background
$color-tasks = lighten($color-herobox, 65%)
// Task filter colors
$color-filter-tag = lighten($good,90%)
$color-filter-tag-challenge = lighten($color-purple,90%)
$color-filter-tag = lighten($good,60%)
$color-filter-tag-challenge = lighten($color-purple,70%)
// Contributor colors
$color-contributor-one = #f57a9d
$color-contributor-two = #b93030

View file

@ -17,13 +17,26 @@ hrpg-button-color-mixin($hrpg-button-color)
> a, > button
background-color: $hrpg-button-color !important
&:active
background-color: darken($hrpg-button-color, 10%) !important
background-color: darken($hrpg-button-color, 61.18%) !important
@media screen and (min-width:768px)
&:hover
background-color: darken($hrpg-button-color, 2.36%) !important
> a, > button, > input
color: darken($hrpg-button-color, 70%) !important
border-color: darken($hrpg-button-color, 16.18%) !important
> input
&:hover
border-color: darken($hrpg-button-color, 32.8%) !important
&:focus
border-color: darken($hrpg-button-color, 61.8%) !important;
outline: none;
> input + button
&:focus
border-color: darken($hrpg-button-color, 32.8%) !important;
background-color: darken($hrpg-button-color, 6.18%) !important;
outline: none;
&:active
background-color: darken($hrpg-button-color, 16.18%) !important;
> a:nth-of-type(2)
border-left: 1px solid darken($hrpg-button-color, 3.82%) !important
> div
@ -48,7 +61,14 @@ hrpg-button-color-mixin($hrpg-button-color)
&.active
a, button
background-color: darken($hrpg-button-color, 3.82%) !important
border-color: darken($hrpg-button-color, 38.2%) !important
border-color: darken($hrpg-button-color, 50%) !important
&.active.filters-tags
a, button
background-color: darken($hrpg-button-color, 32.8%) !important
border-color: $hrpg-button-color !important
color: #fff !important;
span
color: #fff !important;
$hrpg-button-master
list-style: none
> a, > button, > input
@ -61,11 +81,9 @@ $hrpg-button-master
.glyphicon
position: relative
top: 0.132em
> a, > button
&:active
background-color: #aaa !important
@media screen and (min-width:768px)
&:hover
background-color: #eee !important
$hrpg-button
@extend $hrpg-button-master
> a, > button, > input
@ -95,8 +113,11 @@ $hrpg-button-with-input
border: 1px solid #ccc
border-radius: 0.382em 0em 0em 0.382em
padding-left: 0.618em
&:hover
background-color: #f5f5f5 !important
background-color: #fff !important
-webkit-appearance: none
-moz-appearance: none
appearance: none
box-shadow: none //remove red glow in Firefox
a, button
border-width: 1px
border-color: #ccc
@ -105,31 +126,44 @@ $hrpg-button-with-input
border-bottom-style: solid
border-left: none
border-radius: 0em 0.382em 0.382em 0em
outline: none
//Button bar
hrpg-button-bar-mixin($hrpg-button-bar-color)
border-color: darken($hrpg-button-bar-color, 16.18%)
li
border-right-color: darken($hrpg-button-bar-color,3.82%)
border-right-color: darken($hrpg-button-bar-color,6.18%)
&.active
a
box-shadow: 0 0 0 1px darken($hrpg-button-bar-color,23.8%) !important
li:first-of-type
background-color: darken($hrpg-button-bar-color,6.18%)
color: darken($hrpg-button-bar-color,70%)
border-right-color: darken($hrpg-button-bar-color,16.18%)
color: darken($hrpg-button-bar-color,50%);
//border-right-color: darken($hrpg-button-bar-color,16.18%)
$hrpg-button-bar
list-style: none
display: inline-block
border: 1px solid darken(#fff,16.18%)
border-radius: 0.382em
margin-bottom: 0.618em
@extend $clearfix
li
border-right: 1px solid darken(#fff,3.82%)
float:left
li:first-of-type
color: darken(#fff,61.8%)
padding: 0.25em 0.618em
li:nth-of-type(2)
border-radius: 0.382em 0em 0em 0.382em
border-right: 1px solid darken(#fff,6.18%)
li:first-of-type
border-radius: 0.382em 0em 0em 0.382em
a
border-radius: 0.382em 0em 0em 0.382em
li:last-of-type
border-right: none
a
border-radius: 0em 0.382em 0.382em 0em
$hrpg-button-bar-with-heading
@extend $hrpg-button-bar
li:first-of-type
padding: 0.25em 0.618em
// Labels
hrpg-label-color-mixin($hrpg-label-color)
background-color: $hrpg-label-color !important

View file

@ -67,53 +67,20 @@ for $stage in $stages
max-height: 18.6em
overflow-y: scroll
// add new task form
// "Add new task" form
// -----------------
.addtask-form
margin-bottom: 0.75em
outline: 1px solid rgba(0,0,0,0.15)
outline-offset: -1px
background-color: white
// box-shadow: 0 0 3px rgba(0,0,0,0.15)
position: relative
// the input field
.addtask-field
display: block
width: 100%
.task-add
margin-top: 1.618em
@extend $hrpg-button-with-input
hrpg-button-color-mixin($color-options-submenu)
input, button
height: 2.618em
input
font-family: 'Lato', sans-serif
border: 0
outline: 0
border-radius: 0
box-shadow: none
background-color: white
height: 3em
padding: 0 3.3em 0 0.5em
width: 100%
&:focus
box-shadow: inset 0 0 3px darken($best, 20%),inset -1px 0 1px darken($best, 30%)
// the button
.addtask-btn
position: absolute
right: 0
top: 0
// important for overriding bootstrap, remove later
width: 1.81818em !important
height: 1.88em
padding: 0
font-size: 1.61em
line-height: 1.7
outline: 0
border: 0
border-radius: 0 //required to nuke BB-playbook user agent styles
background-color: darken($best, 15%)
&:hover, &:focus
background-color: darken($best, 20%)
&:active
background-color: darken($best, 30%)
width: 80%
button
width: 20%
span
font-size: 0.8em
// an individual task entry
// ------------------------
@ -428,22 +395,6 @@ form
// todos ui
// --------
// nav tabs
.nav-tabs
margin-top: 1.5em
.nav-tabs > li
&.active > a
color: #333
&:hover
margin-top: 1px
> a
border-radius: 0 !important
margin-top: 1px
color: #333
&:hover
border-top: 0
margin-top: 2px
// Checklists
// --------
.checklist-form
@ -475,4 +426,34 @@ form
margin: 0px 10px 3px 0px
width: 4%
li:hover .checklist-icons
opacity:1
opacity:1
// Task filters
// --------
.task-filter
margin: 1.618em 0 1em 0
hrpg-button-bar-mixin($color-tasks)
@extend $hrpg-button-bar
width: 100%
li
@extend $hrpg-button-master
hrpg-button-color-mixin($color-tasks)
width: 33.333%
text-align: center
&:nth-child(3n + 1):nth-last-child(2),
&:nth-child(3n + 1):nth-last-child(2) + li
width: 50%;
a
width: 100%
height: 100%
.rewards
.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%
.repeat-days
a
border-radius: 0.382em

View file

@ -3,7 +3,7 @@
window.habitrpg = angular.module('habitrpg',
['ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices',
'authServices', 'notificationServices', 'guideServices', 'authCtrl',
'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'pasvaz.bindonce', 'infinite-scroll', 'ui.select2'])
'ui.bootstrap', 'ui.keypress', 'ui.router', 'chieffancypants.loadingBar', 'At', 'infinite-scroll', 'ui.select2', 'angular.filter'])
// @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
@ -75,6 +75,11 @@ window.habitrpg = angular.module('habitrpg',
templateUrl: "partials/options.social.html"
})
.state('options.social.inbox', {
url: "/inbox",
templateUrl: "partials/options.social.inbox.html"
})
.state('options.social.tavern', {
url: "/tavern",
templateUrl: "partials/options.social.tavern.html",

View file

@ -7,24 +7,13 @@
angular.module('authCtrl', [])
.controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','ApiUrlService', '$modal',
function($scope, $rootScope, User, $http, $location, $window, ApiUrlService, $modal) {
var runAuth;
var showedFacebookMessage;
$scope.useUUID = false;
$scope.toggleUUID = function() {
if (showedFacebookMessage === false) {
alert(window.env.t('untilNoFace'));
showedFacebookMessage = true;
}
$scope.useUUID = !$scope.useUUID;
};
$scope.logout = function() {
localStorage.clear();
window.location.href = '/logout';
};
runAuth = function(id, token) {
var runAuth = function(id, token) {
User.authenticate(id, token, function(err) {
$window.location.href = '/';
});
@ -59,14 +48,10 @@ angular.module('authCtrl', [])
username: $scope.loginUsername || $('#login-tab input[name="username"]').val(),
password: $scope.loginPassword || $('#login-tab input[name="password"]').val()
};
if ($scope.useUUID) {
runAuth($scope.loginUsername, $scope.loginPassword);
} else {
$http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
}
$http.post(ApiUrlService.get() + "/api/v2/user/auth/local", data)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
};
$scope.playButtonClick = function(){
@ -126,5 +111,22 @@ angular.module('authCtrl', [])
$scope.hasNoNotifications = function() {
return selectNotificationValue(false, false, false, false, true);
}
// ------ Social ----------
hello.init({
facebook : window.env.FACEBOOK_KEY,
});
$scope.socialLogin = function(network){
hello(network).login({scope:'email'}).then(function(auth){
$http.post(ApiUrlService.get() + "/api/v2/user/auth/social", auth)
.success(function(data, status, headers, config) {
runAuth(data.id, data.token);
}).error(errorAlert);
}, function( e ){
alert("Signin error: " + e.error.message );
});
}
}
]);

View file

@ -31,7 +31,7 @@
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', window.env.GA_ID, 'habitrpg.com');
ga('create', window.env.GA_ID, {userId:User.user._id});
ga('require', 'displayfeatures');
ga('send', 'pageview');
}
@ -39,7 +39,17 @@
// Scripts only for desktop
if (!window.env.IS_MOBILE) {
// Add This
$.getScript("//s7.addthis.com/js/250/addthis_widget.js#pubid=lefnire");
//$.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 = {
services_custom:{
name: "Download",
url: window.env.BASE_URL+"/export/avatar-"+User.user._id+".png",
icon: window.env.BASE_URL+"/favicon.ico"
},
services_expanded:addthisServices,
services_compact:addthisServices
};
// Google Charts
$.getScript("//www.google.com/jsapi", function() {

View file

@ -1,7 +1,7 @@
"use strict";
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state',
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state) {
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
$scope.isMemberOfPendingQuest = function(userid, group) {
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
@ -45,8 +45,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
} else {
// We need the member information up top here, but then we pass it down to the modal controller
// down below. Better way of handling this?
Members.selectMember(uid);
$rootScope.openModal('member', {controller:'MemberModalCtrl'});
Members.selectMember(uid, function(){
$rootScope.openModal('member', {controller:'MemberModalCtrl', windowClass:'profile-modal', size:'lg'});
});
}
}
@ -63,7 +64,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}
}
// ------ Invites ------
// ------ Invites ------
$scope.invite = function(group){
Groups.Group.invite({gid: group._id, uuid: group.invitee}, undefined, function(){
@ -72,11 +73,42 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
group.invitee = '';
});
}
//var serializeQs = function(obj, prefix){
// var str = [];
// for(var p in obj) {
// if (obj.hasOwnProperty(p)) {
// var k = prefix ? prefix + "[" + p + "]" : p, v = obj[p];
// str.push(typeof v == "object" ?
// serializeQs(v, k) :
// encodeURIComponent(k) + "=" + encodeURIComponent(v));
// }
// }
// return str.join("&");
//}
//
//$scope.inviteLink = function(obj){
// return window.env.BASE_URL + '?' + serializeQs({partyInvite: obj});
//}
$scope.emails = [{name:"",email:""},{name:"",email:""}];
$scope.inviter = User.user.profile.name;
$scope.inviteEmails = function(inviter, emails){
$http.post('/api/v2/user/social/invite-friends', {inviter:inviter, emails:emails}).success(function(){
Notification.text("Invitations sent!");
$scope.emails = [{name:'',email:''},{name:'',email:''}];
});
}
$scope.quickReply = function(uid) {
Members.selectMember(uid, function(){
$rootScope.openModal('private-message',{controller:'MemberModalCtrl'});
});
}
}
])
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared',
function($scope, $rootScope, Members, Shared) {
.controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification',
function($scope, $rootScope, Members, Shared, $http, Notification) {
$scope.timestamp = function(timestamp){
return moment(timestamp).format('MM/DD/YYYY');
}
@ -86,6 +118,13 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
member.petCount = Shared.countPets(null, member.items.pets);
$scope.profile = member;
});
$scope.sendPrivateMessage = function(uuid, message){
$http.post('/api/v2/members/'+uuid+'/message',{message:message}).success(function(){
Notification.text(window.env.t('messageSentAlert'));
$rootScope.User.sync();
$scope.$close();
});
}
}
])

View file

@ -16,6 +16,10 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams){
if (!!fromState.name) window.ga && ga('send', 'pageview', {page: '/#/'+toState.name});
// clear inbox when entering or exiting inbox tab
if (fromState.name=='options.social.inbox' || toState.name=='options.social.inbox') {
User.user.ops.update && User.set({'inbox.newMessages':0});
}
});
$rootScope.User = User;
@ -113,8 +117,9 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
controller: options.controller, // optional
scope: options.scope, // optional
keyboard: (options.keyboard === undefined ? true : options.keyboard), // optional
backdrop: (options.backdrop === undefined ? true : options.backdrop) // optional
backdrop: (options.backdrop === undefined ? true : options.backdrop), // optional
size: options.size, // optional, 'sm' or 'lg'
windowClass: options.windowClass // optional
});
}

View file

@ -2,8 +2,8 @@
// Make user and settings available for everyone through root scope.
habitrpg.controller('SettingsCtrl',
['$scope', 'User', '$rootScope', '$http', 'ApiUrlService', 'Guide', '$location', '$timeout', 'Notification',
function($scope, User, $rootScope, $http, ApiUrlService, Guide, $location, $timeout, Notification) {
['$scope', 'User', '$rootScope', '$http', 'ApiUrlService', 'Guide', '$location', '$timeout', 'Notification', 'Shared',
function($scope, User, $rootScope, $http, ApiUrlService, Guide, $location, $timeout, Notification, Shared) {
// FIXME we have this re-declared everywhere, figure which is the canonical version and delete the rest
// $scope.auth = function (id, token) {
@ -162,5 +162,22 @@ habitrpg.controller('SettingsCtrl',
User.user.ops.release2({});
$rootScope.$state.go('tasks');
}
// ---- Webhooks ------
$scope._newWebhook = {url:''};
$scope.$watch('user.preferences.webhooks',function(webhooks){
$scope.hasWebhooks = _.size(webhooks);
})
$scope.addWebhook = function(url) {
User.user.ops.addWebhook({body:{url:url, id:Shared.uuid()}});
$scope._newWebhook.url = '';
}
$scope.saveWebhook = function(id,webhook) {
delete webhook._editing;
User.user.ops.updateWebhook({params:{id:id}, body:webhook});
}
$scope.deleteWebhook = function(id) {
User.user.ops.deleteWebhook({params:{id:id}});
}
}
]);

View file

@ -57,7 +57,7 @@ angular.module('memberServices', ['ngResource', 'sharedServices']).
* either gets them or fetches if not available
* @param uid
*/
selectMember: function(uid) {
selectMember: function(uid, cb) {
var self = this;
// Fetch from cache if we can. For guild members, only their uname will have been fetched on initial load,
// check if they have full fields (eg, check profile.items and an item inside
@ -67,11 +67,13 @@ angular.module('memberServices', ['ngResource', 'sharedServices']).
if (members[uid] && members[uid].items && members[uid].items.weapon) {
Shared.wrap(members[uid],false);
self.selectedMember = members[uid];
cb();
} else {
Member.get({uid: uid}, function(member){
self.populate(member); // lazy load for later
Shared.wrap(member,false);
self.selectedMember = members[member._id];
cb();
});
}
}

View file

@ -6,13 +6,29 @@ window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices'
.constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings')
.constant("MOBILE_APP", false)
habitrpgStatic.controller("PlansCtrl", ['$rootScope',
.controller("RootCtrl", ['$scope', '$location', '$modal', '$http', function($scope, $location, $modal, $http){
// must be #?memberId=xx, see https://github.com/angular/angular.js/issues/7239
var memberId = $location.search()['memberId'];
if (memberId) {
$http.get('/api/v2/members/'+memberId).success(function(data, status, headers, config){
$scope.profile = data;
$scope.Content = window.habitrpgShared.content;
$modal.open({
templateUrl: 'modals/member.html',
scope: $scope
});
})
}
}])
.controller("PlansCtrl", ['$rootScope',
function($rootScope) {
$rootScope.clickContact = function(){
window.ga && ga('send', 'event', 'button', 'click', 'Contact Us (Plans)');
}
}
])
.controller('AboutCtrl',[function(){
$(document).ready(function(){
$('a.gallery').colorbox({

View file

@ -15,12 +15,13 @@
"bower_components/angular-loading-bar/build/loading-bar.js",
"bower_components/Angular-At-Directive/src/at.js",
"bower_components/Angular-At-Directive/src/caret.js",
"bower_components/angular-bindonce/bindonce.js",
"bower_components/js-emoji/emoji.js",
"bower_components/sticky/jquery.sticky.js",
"bower_components/ngInfiniteScroll/build/ng-infinite-scroll.min.js",
"bower_components/select2/select2.js",
"bower_components/angular-ui-select2/src/select2.js",
"bower_components/hello/dist/hello.all.min.js",
"bower_components/angular-filter/dist/angular-filter.min.js",
"bower_components/angular-bootstrap/ui-bootstrap.js",
"bower_components/angular-bootstrap/ui-bootstrap-tpls.js",
@ -66,6 +67,7 @@
],
"css": [
"bower_components/bootstrap/dist/css/bootstrap.css",
"bower_components/css-social-buttons/css/zocial.css",
"app.css",
"bower_components/pnotify/jquery.pnotify.default.css",
"bower_components/pnotify/jquery.pnotify.default.icons.css",
@ -83,6 +85,7 @@
"bower_components/angular-bootstrap/ui-bootstrap-tpls.js",
"bower_components/bootstrap/dist/js/bootstrap.js",
"bower_components/jquery-colorbox/jquery.colorbox-min.js",
"bower_components/hello/dist/hello.all.min.js",
"bower_components/angular-loading-bar/build/loading-bar.js",
"js/env.js",
@ -94,7 +97,10 @@
],
"css": [
"bower_components/bootstrap/dist/css/bootstrap.css",
"bower_components/css-social-buttons/css/zocial.css",
"bower_components/jquery-colorbox/example1/colorbox.css",
"app.css",
"bower_components/habitrpg-shared/dist/habitrpg-shared.css",
"static.css"
]
}

View file

@ -24,31 +24,6 @@ var accountSuspended = function(uuid){
};
}
var emailUser = function(name, email, emailType){
request({
url: nconf.get('EMAIL_SERVER_URL') + '/job',
method: 'POST',
auth: {
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD')
},
json: {
type: 'email',
data: {
emailType: emailType,
to: {
name: name,
email: email
}
},
options: {
attemps: 5,
backoff: {delay: 10*60*1000, type: 'fixed'}
}
}
});
}
api.auth = function(req, res, next) {
var uid = req.headers['x-api-user'];
var token = req.headers['x-api-key'];
@ -128,7 +103,7 @@ api.registerUser = function(req, res, next) {
}
user.save(cb);
if(isProd) emailUser(username, email, 'welcome');
if(isProd) utils.txnEmail({name:username, email:email}, 'welcome');
ga.event('register', 'Local').send()
}
], function(err, saved) {
@ -166,26 +141,58 @@ api.loginLocal = function(req, res, next) {
};
/*
POST /user/auth/facebook
POST /user/auth/social
*/
api.loginSocial = function(req, res, next) {
var access_token = req.body.authResponse.access_token,
network = req.body.network;
if (network!=='facebook')
return res.json(401, {err:"Only Facebook supported currently."});
async.waterfall([
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);
// Create new user
var prof = data.profile;
var user = {
preferences: {
language: req.language // User language detected from browser, not saved
},
auth: {
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
};
user.auth[network] = prof;
user = new User(user);
user.save(cb);
api.loginFacebook = function(req, res, next) {
var facebook_id = req.body.facebook_id;
if (!facebook_id) return res.json(401, {err: 'No facebook id provided'});
User.findOne({'auth.facebook.id': facebook_id}, function(err, user) {
if (err) {
return res.json(401, {err: err});
} else if (user) {
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
return res.json(200, {id: user.id,token: user.apiToken});
} else {
/* FIXME: create a new user instead*/
return res.json(403, {err: "Please register with Facebook on https://habitrpg.com, then come back here and log in."});
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){
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});
})
};
/**
* DELETE /user/auth/social
* TODO implement
*/
api.deleteSocial = function(req,res,next){next()}
api.resetPassword = function(req, res, next){
var email = req.body.email,
salt = utils.makeSalt(),
@ -271,66 +278,4 @@ api.setupPassport = function(router) {
res.redirect('/');
})
// GET /auth/facebook
// Use passport.authenticate() as route middleware to authenticate the
// request. The first step in Facebook authentication will involve
// redirecting the user to facebook.com. After authorization, Facebook will
// redirect the user back to this application at /auth/facebook/callback
router.get('/auth/facebook',
passport.authenticate('facebook', {scope: 'email'}),
i18n.getUserLanguage,
function(req, res){
// The request will be redirected to Facebook for authentication, so this
// function will not be called.
});
// GET /auth/facebook/callback
// Use passport.authenticate() as route middleware to authenticate the
// request. If authentication fails, the user will be redirected back to the
// login page. Otherwise, the primary route function function will be called,
// which, in this example, will redirect the user to the home page.
router.get('/auth/facebook/callback',
passport.authenticate('facebook', { failureRedirect: '/login' }),
i18n.getUserLanguage,
function(req, res) {
//res.redirect('/');
async.waterfall([
function(cb){
User.findOne({'auth.facebook.id':req.user.id}, cb)
},
function(user, cb){
if (user) return cb(null, user);
user = new User({
preferences: {
language: req.language // User language detected from browser, not saved
},
auth: {
facebook: req.user,
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
});
user.save(cb);
if(isProd && req.user.emails && req.user.emails[0] && req.user.emails[0].value){
emailUser((req.user.displayName || req.user.username), req.user.emails[0].value, 'welcome');
}
ga.event('register', 'Facebook').send()
}
], function(err, saved){
if (err) return res.redirect('/static/front?err=' + err);
req.session.userId = saved._id;
res.redirect('/static/front?_id='+saved._id+'&apiToken='+saved.apiToken);
})
});
// Simple route middleware to ensure user is authenticated.
// Use this route middleware on any resource that needs to be protected. If
// the request is authenticated (typically via a persistent login session),
// the request will proceed. Otherwise, the user will be redirected to the
// login page.
// function ensureAuthenticated(req, res, next) {
// if (req.isAuthenticated()) { return next(); }
// res.redirect('/login')
// }
};

View file

@ -8,6 +8,13 @@ var js2xmlparser = require("js2xmlparser");
var pd = require('pretty-data').pd;
var User = require('../models/user').model;
// Avatar screenshot/static-page includes
var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres
var AWS = require('aws-sdk');
AWS.config.update({accessKeyId: nconf.get("S3:accessKeyId"), secretAccessKey: nconf.get("S3:secretAccessKey")});
var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream
var bucket = nconf.get("S3:bucket");
var request = require('request');
/*
------------------------------------------------------------------------
@ -83,3 +90,49 @@ expressres.jsonstring = function(obj, headers, status) {
body = pd.json(JSON.stringify(obj));
return this.send(body, headers, status);
};
/*
------------------------------------------------------------------------
Static page and image screenshot of avatar
------------------------------------------------------------------------
*/
dataexport.avatarPage = function(req, res) {
User.findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){
res.render('avatar-static', {
title: user.profile.name,
env: _.defaults({user:user},res.locals.habitrpg)
});
})
};
dataexport.avatarImage = function(req, res, next) {
var filename = 'avatar-'+req.params.uuid+'.png';
request.head('https://'+bucket+'.s3.amazonaws.com/'+filename, function(err,response,body) {
// cache images for 10 minutes on aws, else upload a new one
if (response.statusCode==200 && moment().diff(response.headers['last-modified'], 'minutes') < 10)
return res.redirect(301, 'https://' + bucket + '.s3.amazonaws.com/' + filename);
new Pageres()//{delay:1}
.src(nconf.get('BASE_URL') + '/export/avatar-' + req.params.uuid + '.html', ['140x147'], {crop: true, filename: filename.replace('.png', '')})
.run(function (err, file) {
if (err) return next(err);
// see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createMultipartUpload-property
var upload = s3Stream.upload({
Bucket: bucket,
Key: filename,
ACL: "public-read",
StorageClass: "REDUCED_REDUNDANCY",
ContentType: "image/png",
Expires: +moment().add({minutes: 3})
});
upload.on('error', function (err) {
next(err);
});
upload.on('uploaded', function (details) {
res.redirect(details.Location);
});
file[0].pipe(upload);
});
})
};

View file

@ -19,7 +19,7 @@ var api = module.exports;
------------------------------------------------------------------------
*/
var partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items';
var partyFields = api.partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items';
var nameFields = 'profile.name';
var challengeFields = '_id name';
var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} };
@ -44,15 +44,6 @@ var populateQuery = function(type, q){
return q;
}
api.getMember = function(req, res, next) {
User.findById(req.params.uid).select(partyFields).exec(function(err, user){
if (err) return next(err);
if (!user) return res.json(400,{err:'User not found'});
res.json(user);
})
}
/**
* Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually
* as /groups/party or /groups/tavern
@ -300,7 +291,8 @@ api.join = function(req, res, next) {
group = res.locals.group;
if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) {
user.invitations.party = undefined;
User.update({_id:user.invitations.party.inviter}, {$inc:{'items.quests.basilist':1}}).exec(); // Reward inviter
user.invitations.party = undefined; // Clear invite
user.save();
// invite new user to pending quest
if (group.quest.key && !group.quest.active) {
@ -439,10 +431,10 @@ api.invite = function(req, res, next) {
function sendInvite (){
if(group.type === 'guild'){
invite.invitations.guilds.push({id: group._id, name: group.name});
invite.invitations.guilds.push({id: group._id, name: group.name, inviter:res.locals.user._id});
}else{
//req.body.type in 'guild', 'party'
invite.invitations.party = {id: group._id, name: group.name}
invite.invitations.party = {id: group._id, name: group.name, inviter:res.locals.user._id};
}
group.invites.push(invite._id);

View file

@ -55,12 +55,12 @@ api.updateHero = function(req,res,next) {
function(member, cb){
if (!member) return res.json(404, {err: "User not found"});
member.balance = req.body.balance || 0;
newTier = req.body.contributor.level; // tier = level in this context
oldTier = member.contributor && member.contributor.level || 0;
var newTier = req.body.contributor.level; // tier = level in this context
var oldTier = member.contributor && member.contributor.level || 0;
if (newTier > oldTier) {
member.flags.contributor = true;
gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff
tierDiff = newTier - oldTier; // can be 2+ tier increases at once
var gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff
var tierDiff = newTier - oldTier; // can be 2+ tier increases at once
while (tierDiff) {
member.balance += gemsPerTier[newTier] / 4; // balance is in $
tierDiff--;
@ -71,7 +71,7 @@ api.updateHero = function(req,res,next) {
member.purchased.ads = req.body.purchased.ads;
if (member.contributor.level >= 6) member.items.pets['Dragon-Hydra'] = 5;
if (req.body.itemPath && req.body.itemVal
&& req.body.itemPath.indexOf('items.')===0
&& req.body.itemPath.indexOf('items.') === 0
&& User.schema.paths[req.body.itemPath]) {
shared.dotSet(member, req.body.itemPath, req.body.itemVal); // Sanitization at 5c30944 (deemed unnecessary)
}

View file

@ -0,0 +1,50 @@
var User = require('mongoose').model('User');
var groups = require('../models/group');
var partyFields = require('./groups').partyFields
var api = module.exports;
var async = require('async');
var _ = require('lodash');
var shared = require('habitrpg-shared');
api.getMember = function(req, res, next) {
User.findById(req.params.uuid).select(partyFields).exec(function(err, user){
if (err) return next(err);
if (!user) return res.json(400,{err:'User not found'});
res.json(user);
})
}
api.sendPrivateMessage = function(req,res,next){
async.waterfall([
function(cb){
User.findById(req.params.uuid, cb);
},
function(member, cb){
if (!member) return cb({code:404, err: 'User not found'});
if (~member.inbox.blocks.indexOf(res.locals.user._id) // can't send message if that user blocked me
|| ~res.locals.user.inbox.blocks.indexOf(member._id) // or if I blocked them
|| member.inbox.optOut) { // or if they've opted out of messaging
return cb({code:401, err: "Can't send message to this user."});
}
var message = groups.chatDefaults(req.body.message, res.locals.user);
shared.refPush(member.inbox.messages, message);
member.inbox.newMessages++;
member._v++;
member.markModified('inbox.messages');
var message = groups.chatDefaults(req.body.message, member);
shared.refPush(res.locals.user.inbox.messages, _.defaults({sent:true},message));
res.locals.user.markModified('inbox.messages');
member.save(cb);
},
function(a,b,cb){
res.locals.user.save(cb);
}
], function(err){
if (err) return err.code ? res.json(err.code,{err:err.err}) : err;
res.send(200);
})
}

View file

@ -1,324 +0,0 @@
/* @see ./routes.coffee for routing*/
var _ = require('lodash');
var logger = require('../logging');
var ipn = require('paypal-ipn');
var shared = require('habitrpg-shared');
var nconf = require('nconf');
var async = require('async');
var User = require('./../models/user').model;
var ga = require('./../utils').ga;
var logging = require('./../logging');
var userAPI = require('./user');
var request = require('request');
var moment = require('moment');
var api = module.exports;
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require("stripe")(nconf.get('STRIPE_API_KEY'));
var PaypalRecurring = require('paypal-recurring');
var paypalRecurring = new PaypalRecurring({
username: nconf.get('PAYPAL_USERNAME'),
password: nconf.get('PAYPAL_PASSWORD'),
signature: nconf.get('PAYPAL_SIGNATURE')
}, isProduction ? "production" : "sandbox");
var paypalCheckout = require('paypal-express-checkout')
.init(nconf.get('PAYPAL_USERNAME'), nconf.get('PAYPAL_PASSWORD'), nconf.get('PAYPAL_SIGNATURE'), nconf.get('BASE_URL'), nconf.get('BASE_URL'), !isProduction);
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
if (
item.klass === 'mystery' &&
moment().isAfter(item.mystery.start) &&
moment().isBefore(item.mystery.end) &&
!user.items.gear.owned[item.key] &&
!~user.purchased.plan.mysteryItems.indexOf(item.key)
) {
user.purchased.plan.mysteryItems.push(item.key);
}
});
}
function getMailingInfo(user) {
var email, name;
if(user.auth.local){
email = user.auth.local.email;
name = user.profile.name || user.auth.local.username;
}else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){
email = user.auth.facebook.emails[0].value;
name = user.auth.facebook.displayName || user.auth.facebook.username;
}
return {'email': email, 'name': name};
}
function emailUser(user, emailType) {
var mailingInfo = getMailingInfo(user);
if(mailingInfo.email){
request({
url: nconf.get('EMAIL_SERVER_URL') + '/job',
method: 'POST',
auth: {
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
pass: nconf.get('EMAIL_SERVER_AUTH_PASSWORD')
},
json: {
type: 'email',
data: {
emailType: emailType,
to: {
name: mailingInfo.name,
email: mailingInfo.email
}
},
options: {
attemps: 5,
backoff: {delay: (10*60*1000), type: 'fixed'}
}
}
});
}
}
function createSubscription(user, data) {
if (!user.purchased.plan) user.purchased.plan = {};
_(user.purchased.plan)
.merge({ // override with these values
planId:'basic_earned',
customerId: data.customerId,
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: data.paymentMethod,
dateTerminated: null
})
.defaults({ // allow non-override if a plan was previously used
dateCreated: new Date(),
mysteryItems: []
});
revealMysteryItems(user);
if(isProduction) emailUser(user, 'subscription-begins');
user.purchased.txnCount++;
ga.event('subscribe', data.paymentMethod).send();
ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send();
}
/**
* Sets their subscription to be cancelled later
*/
function cancelSubscription(user, data){
var du = user.purchased.plan.dateUpdated, now = moment();
if(isProduction) emailUser(user, 'cancel-subscription');
user.purchased.plan.dateTerminated =
moment( now.format('MM') + '/' + moment(du).format('DD') + '/' + now.format('YYYY') )
.add({months:1})
.toDate();
ga.event('unsubscribe', 'Stripe').send();
}
function buyGems(user, data) {
user.balance += 5;
user.purchased.txnCount++;
if(isProduction) emailUser(user, 'donation');
ga.event('checkout', data.paymentMethod).send();
ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
}
// Expose some functions for tests
if (nconf.get('NODE_ENV')==='testing') {
api.cancelSubscription = cancelSubscription;
api.createSubscription = createSubscription;
}
/*
Setup Stripe response when posting payment
*/
api.stripeCheckout = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
async.waterfall([
function(cb){
if (req.query.plan) {
stripe.customers.create({
email: req.body.email,
metadata: {uuid: res.locals.user._id},
card: token,
plan: req.query.plan,
}, cb);
} else {
stripe.charges.create({
amount: "500", // $5
currency: "usd",
card: token
}, cb);
}
},
function(response, cb) {
if (req.query.plan) {
createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'});
} else {
buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'});
}
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.send(200);
user = token = null;
});
};
api.stripeSubscribeCancel = function(req, res, next) {
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
async.waterfall([
function(cb) {
stripe.customers.del(user.purchased.plan.customerId, cb);
},
function(response, cb) {
cancelSubscription(user);
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.redirect('/');
user = null;
});
};
api.stripeSubscribeEdit = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
var user_id = user.purchased.plan.customerId;
var sub_id;
async.waterfall([
function(cb){
stripe.customers.listSubscriptions(user_id, cb);
},
function(response, cb) {
sub_id = response.data[0].id;
console.warn(sub_id);
console.warn([user_id, sub_id, { card: token }]);
stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb);
},
function(response, cb) {
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.send(200);
token = user = user_id = sub_id;
});
};
api.paypalSubscribe = function(req,res,next) {
// Authenticate a future subscription of ~5 USD
paypalRecurring.authenticate({
RETURNURL: nconf.get('BASE_URL') + '/paypal/subscribe/success?uuid=' + res.locals.user._id,
CANCELURL: nconf.get("BASE_URL"),
PAYMENTREQUEST_0_AMT: 5,
L_BILLINGAGREEMENTDESCRIPTION0: "HabitRPG Subscription"
}, function(err, data, url) {
// Redirect the user if everything went well with
// a HTTP 302 according to PayPal's guidelines
if (err) return next(err);
res.redirect(302, url);
});
};
api.paypalSubscribeSuccess = function(req,res,next) {
// Create a subscription of 10 USD every month
var uuid = req.query.uuid;
if (!uuid) return next("UUID required");
paypalRecurring.createSubscription(req.query.token, req.query.PayerID,{
AMT: 5,
DESC: "HabitRPG Subscription",
BILLINGPERIOD: "Month",
BILLINGFREQUENCY: 1,
}, function(err, data) {
if (err) return res.next(err);
User.findById(uuid, function(err,user){
if (err) return next(err);
createSubscription(user, {customerId: data.PROFILEID, paymentMethod: 'Paypal'});
user.save(function(err,saved){
res.redirect('/');
})
})
});
};
api.paypalSubscribeCancel = function(req, res, next) {
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
async.waterfall([
function(cb) {
paypalRecurring.modifySubscription(user.purchased.plan.customerId, 'cancel', cb);
},
function(response, cb) {
cancelSubscription(user);
user.save(cb);
}
], function(err, saved){
if (err) return next(err);
res.redirect('/');
user = null;
});
};
api.paypalCheckout = function(req, res, next) {
var opts = {RETURNURL:nconf.get('BASE_URL') + '/paypal/checkout/success?uuid=' + res.locals.user._id};
paypalCheckout.pay(+new Date(), 5, 'HabitRPG Gems', 'USD', opts, function(err, url) {
if (err) return next(err);
res.redirect(url);
});
};
api.paypalCheckoutSuccess = function(req,res,next) {
paypalCheckout.detail(req.query.token, req.query.PayerID, function(err, data, invoiceNumber, price) {
// see `data` vars at https://github.com/petersirka/node-paypal-express-checkout#paypal-account
//if (err) return next('PayPal Error: ' + msg);
if (err) return next(err);
if (data.ACK !== 'Success') return next('PayPal transaction failed, please try again');
var uuid = req.query.uuid; //, apiToken = query.apiToken;
User.findById(uuid , function(err, user) {
if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction";
if (err) return next(err);
buyGems(user, {customerId:req.query.PayerID, paymentMethod:'Paypal'});
user.save(function(){
if (err) return next(err);
res.redirect('/');
uuid = null;
});
});
});
};
/**
* General IPN handler. We could use this for all paypal transaction handling (instead of the above functions), but I've
* found it extremely unreliable. Instead, here we'll cancel HabitRPG subscriptions for users who manually cancel their
* recurring paypal payments.
*/
api.paypalIPN = function(req, res, next) {
// Must respond to PayPal IPN request with an empty 200 first, if using Express uncomment the following:
res.send(200);
ipn.verify(req.body, function callback(err, msg) {
if (err) return logger.error(msg);
switch (req.body.txn_type) {
// TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead...
case 'recurring_payment_profile_cancel':
case 'subscr_cancel':
User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
if (err) return logger.error(err);
if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel)
cancelSubscription(user);
user.save();
});
break;
}
});
};

View file

@ -0,0 +1,77 @@
/* @see ./routes.coffee for routing*/
var _ = require('lodash');
var shared = require('habitrpg-shared');
var nconf = require('nconf');
var utils = require('./../../utils');
var moment = require('moment');
var isProduction = nconf.get("NODE_ENV") === "production";
var stripe = require('./stripe');
var paypal = require('./paypal');
function revealMysteryItems(user) {
_.each(shared.content.gear.flat, function(item) {
if (
item.klass === 'mystery' &&
moment().isAfter(item.mystery.start) &&
moment().isBefore(item.mystery.end) &&
!user.items.gear.owned[item.key] &&
!~user.purchased.plan.mysteryItems.indexOf(item.key)
) {
user.purchased.plan.mysteryItems.push(item.key);
}
});
}
exports.createSubscription = function(user, data) {
if (!user.purchased.plan) user.purchased.plan = {};
_(user.purchased.plan)
.merge({ // override with these values
planId:'basic_earned',
customerId: data.customerId,
dateUpdated: new Date(),
gemsBought: 0,
paymentMethod: data.paymentMethod,
dateTerminated: null
})
.defaults({ // allow non-override if a plan was previously used
dateCreated: new Date(),
mysteryItems: []
});
revealMysteryItems(user);
if(isProduction) utils.txnEmail(user, 'subscription-begins');
user.purchased.txnCount++;
utils.ga.event('subscribe', data.paymentMethod).send();
utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod + " > Stripe").send();
}
/**
* Sets their subscription to be cancelled later
*/
exports.cancelSubscription = function(user, data) {
var du = user.purchased.plan.dateUpdated, now = moment();
if(isProduction) utils.txnEmail(user, 'cancel-subscription');
user.purchased.plan.dateTerminated =
moment( now.format('MM') + '/' + moment(du).format('DD') + '/' + now.format('YYYY') )
.add({months:1})
.toDate();
utils.ga.event('unsubscribe', 'Stripe').send();
}
exports.buyGems = function(user, data) {
user.balance += 5;
user.purchased.txnCount++;
if(isProduction) utils.txnEmail(user, 'donation');
utils.ga.event('checkout', data.paymentMethod).send();
utils.ga.transaction(data.customerId, 5).item(5, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
}
exports.stripeCheckout = stripe.checkout;
exports.stripeSubscribeCancel = stripe.subscribeCancel;
exports.stripeSubscribeEdit = stripe.subscribeEdit;
exports.paypalSubscribe = paypal.createBillingAgreement;
exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
exports.paypalSubscribeCancel = paypal.cancelSubscription;
exports.paypalCheckout = paypal.createPayment;
exports.paypalCheckoutSuccess = paypal.executePayment;
exports.paypalIPN = paypal.ipn;

View file

@ -0,0 +1,166 @@
var nconf = require('nconf');
var moment = require('moment');
var async = require('async');
var _ = require('lodash');
var url = require('url');
var mongoose = require('mongoose');
var payments = require('./index');
var logger = require('../../logging');
var ipn = require('paypal-ipn');
var paypal = require('paypal-rest-sdk');
// 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
var billingPlanID = nconf.get('PAYPAL:billing_plan_id');
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
'client_id': nconf.get("PAYPAL:client_id"),
'client_secret': nconf.get("PAYPAL:client_secret")
});
var parseErr = function(err){
return (err.response && err.response.message || err.response.details[0].issue) || err;
}
exports.createBillingAgreement = function(req,res,next){
var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)";
var billingAgreementAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"start_date": moment().add({seconds:5}).format(),
"plan": {
"id": billingPlanID
},
"payer": {
"payment_method": "paypal"
}
};
paypal.billingAgreement.create(billingAgreementAttributes, function (err, billingAgreement) {
if (err) return next(parseErr(err));
// For approving subscription via Paypal, first redirect user to: approval_url
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
res.redirect(approval_url);
});
}
exports.executeBillingAgreement = function(req,res,next){
async.waterfall([
function(cb){
paypal.billingAgreement.execute(req.query.token, {}, cb);
},
function(billingAgreement, cb){
mongoose.model('User').findById(req.session.userId, function(err, user){
if (err) return cb(err);
cb(null, {billingAgreement:billingAgreement, user:user});
});
},
function(data, cb){
payments.createSubscription(data.user, {customerId: data.billingAgreement.id, paymentMethod: 'Paypal'});
data.user.save(cb);
}
],function(err){
if (err) return next(parseErr(err));
res.redirect('/');
})
}
exports.createPayment = function(req, res, next) {
var create_payment = {
"intent": "sale",
"payer": {
"payment_method": "paypal"
},
"redirect_urls": {
"return_url": nconf.get('BASE_URL') + '/paypal/checkout/success',
"cancel_url": nconf.get('BASE_URL')
},
"transactions": [{
"item_list": {
"items": [{
"name": "HabitRPG Gems",
//"sku": "1",
"price": "5.00",
"currency": "USD",
"quantity": 1
}]
},
"amount": {
"currency": "USD",
"total": "5.00"
},
"description": "HabitRPG Gems"
}]
};
paypal.payment.create(create_payment, function (err, payment) {
if (err) return next(parseErr(err));
var link = _.find(payment.links, {rel: 'approval_url'}).href;
res.redirect(link);
});
}
exports.executePayment = function(req, res, next) {
var paymentId = req.query.paymentId,
PayerID = req.query.PayerID;
async.waterfall([
function(cb){
paypal.payment.execute(paymentId, {payer_id: PayerID}, cb);
},
function(payment, cb){
mongoose.model('User').findById(req.session.userId, cb);
},
function(user, cb){
if (_.isEmpty(user)) return cb("user not found when completing paypal transaction");
payments.buyGems(user, {customerId:PayerID, paymentMethod:'Paypal'});
user.save(cb);
}
],function(err, saved){
if (err) return next(parseErr(err));
res.redirect('/');
})
}
exports.cancelSubscription = function(req, res, next){
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
async.waterfall([
function(cb) {
paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb);
},
function(response, cb) {
payments.cancelSubscription(user);
user.save(cb);
}
], function(err, saved){
if (err) return next(parseErr(err));
res.redirect('/');
user = null;
});
}
/**
* General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their
* recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution
*/
exports.ipn = function(req, res, next) {
console.log('IPN Called');
res.send(200); // Must respond to PayPal IPN request with an empty 200 first
ipn.verify(req.body, function(err, msg) {
if (err) return logger.error(msg);
switch (req.body.txn_type) {
// TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead...
case 'recurring_payment_profile_cancel':
case 'subscr_cancel':
mongoose.model('User').findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
if (err) return logger.error(err);
if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel)
payments.cancelSubscription(user);
user.save();
});
break;
}
});
};

View file

@ -0,0 +1,68 @@
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
// and once for any time you need to edit the plan thereafter
var path = require('path');
var nconf = require('nconf');
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
var paypal = require('paypal-rest-sdk');
var OP = "list"; // list create update remove
paypal.configure({
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
'client_id': nconf.get("PAYPAL:client_id"),
'client_secret': nconf.get("PAYPAL:client_secret")
});
var billingPlanTitle ="HabitRPG subscription ($5 month-to-month)";
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements
var billingPlanAttributes = {
"name": billingPlanTitle,
"description": billingPlanTitle,
"type": "INFINITE",
"merchant_preferences": {
"auto_bill_amount": "yes",
"cancel_url": nconf.get("BASE_URL"),
"return_url": nconf.get('BASE_URL') + '/paypal/subscribe/success'
},
"payment_definitions": [{
"name": billingPlanTitle,
"type": "REGULAR",
"frequency_interval": "1",
"frequency": "MONTH",
"cycles": "0",
"amount": {
"currency": "USD",
"value": "5"
}
}]
};
switch(OP) {
case "list":
paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){
console.log({err:err, plans:plans});
});
break;
case "update":
break;
case "create":
paypal.billingPlan.create(billingPlanAttributes, function(err,plan){
if (plan.state == "ACTIVE")
return console.log({err:err, plan:plan});
var billingPlanUpdateAttributes = [{
"op": "replace",
"path": "/",
"value": {
"state": "ACTIVE"
}
}];
// Activate the plan by changing status to Active
paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){
console.log({err:err, response:response});
});
});
case "remove":
break;
}

View file

@ -0,0 +1,89 @@
var nconf = require('nconf');
var stripe = require("stripe")(nconf.get('STRIPE_API_KEY'));
var async = require('async');
var payments = require('./index');
/*
Setup Stripe response when posting payment
*/
exports.checkout = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
async.waterfall([
function(cb){
if (req.query.plan) {
stripe.customers.create({
email: req.body.email,
metadata: {uuid: res.locals.user._id},
card: token,
plan: req.query.plan
}, cb);
} else {
stripe.charges.create({
amount: "500", // $5
currency: "usd",
card: token
}, cb);
}
},
function(response, cb) {
if (req.query.plan) {
payments.createSubscription(user, {customerId: response.id, paymentMethod: 'Stripe'});
} else {
payments.buyGems(user, {customerId: response.id, paymentMethod: 'Stripe'});
}
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.send(200);
user = token = null;
});
};
exports.subscribeCancel = function(req, res, next) {
var user = res.locals.user;
if (!user.purchased.plan.customerId)
return res.json(401, {err: "User does not have a plan subscription"});
async.waterfall([
function(cb) {
stripe.customers.del(user.purchased.plan.customerId, cb);
},
function(response, cb) {
payments.cancelSubscription(user);
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.redirect('/');
user = null;
});
};
exports.subscribeEdit = function(req, res, next) {
var token = req.body.id;
var user = res.locals.user;
var user_id = user.purchased.plan.customerId;
var sub_id;
async.waterfall([
function(cb){
stripe.customers.listSubscriptions(user_id, cb);
},
function(response, cb) {
sub_id = response.data[0].id;
console.warn(sub_id);
console.warn([user_id, sub_id, { card: token }]);
stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb);
},
function(response, cb) {
user.save(cb);
}
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.send(200);
token = user = user_id = sub_id;
});
};

View file

@ -7,13 +7,16 @@ var nconf = require('nconf');
var async = require('async');
var shared = require('habitrpg-shared');
var User = require('./../models/user').model;
var ga = require('./../utils').ga;
var utils = require('./../utils');
var ga = utils.ga;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
var moment = require('moment');
var logging = require('./../logging');
var acceptablePUTPaths;
var api = module.exports;
var qs = require('qs');
var request = require('request');
// api.purchase // Shared.ops
@ -106,6 +109,15 @@ api.score = function(req, res, next) {
_tmp: user._tmp
}, saved.toJSON().stats));
// Webhooks
_.each(user.preferences.webhooks, function(h){
request.post({
url: h.url,
//form: {task: task, delta: delta, user: _.pick(user, ['stats', '_tmp'])} // this is causing "Maximum Call Stack Exceeded"
body: {direction:direction, task: task, delta: delta, user: _.pick(user, ['_id', 'stats', '_tmp'])}, json:true
});
});
if (
(!task.challenge || !task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there
|| (task.type == 'reward') // we don't want to update the reward GP cost
@ -204,7 +216,7 @@ api.getUser = function(req, res, next) {
* FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
*/
acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function(m,v,leaf){
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats'.split(' '), function(root){
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats inbox'.split(' '), function(root){
return leaf.indexOf(root) == 0;
});
if (found) m[leaf]=true;
@ -393,6 +405,51 @@ api.cast = function(req, res, next) {
}
}
/**
* POST /user/invite-friends
*/
api.inviteFriends = function(req, res, next) {
Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){
if (err) return next(err);
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
_.each(req.body.emails, function(invite){
if (invite.email) {
var variables = [
{name: 'LINK', content: link},
{name: 'INVITER', content: req.body.inviter || res.locals.user.profile.name},
{name: 'INVITEE', content: invite.name}
];
// TODO implement "users can only be invited once"
utils.txnEmail(invite, 'invite-friend', variables);
}
});
res.send(200);
})
}
api.sessionPartyInvite = function(req,res,next){
if (!req.session.partyInvite) return next();
var inv = res.locals.user.invitations;
if (inv.party && inv.party.id) return next(); // already invited to a party
async.waterfall([
function(cb){
Group.findOne({_id:req.session.partyInvite.id, type:'party', members:{$in:[req.session.partyInvite.inviter]}})
.select('invites members').exec(cb);
},
function(group, cb){
if (!group) return cb("Inviter not in party");
inv.party = req.session.partyInvite;
delete req.session.partyInvite;
if (!~group.invites.indexOf(res.locals.user._id))
group.invites.push(res.locals.user._id); //$addToSt
group.save(cb);
},
function(saved, cb){
res.locals.user.save(cb);
}
], next);
}
/**
* All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping
*/

View file

@ -12,6 +12,7 @@ var shared = require('habitrpg-shared');
var request = require('request');
var os = require('os');
var moment = require('moment');
var utils = require('./utils');
module.exports.apiThrottle = function(app) {
if (nconf.get('NODE_ENV') !== 'production') return;
@ -192,10 +193,16 @@ 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{
req.session.partyInvite = JSON.parse(utils.decrypt(req.query.partyInvite))
} catch(e){}
next();
}

View file

@ -93,7 +93,7 @@ GroupSchema.methods.toJSON = function(){
return doc;
}
var chatDefaults = function(message,user){
var chatDefaults = module.exports.chatDefaults = function(message,user){
var message = {
id: shared.uuid(),
text: message,

View file

@ -117,6 +117,9 @@ var UserSchema = new Schema({
freeRebirth: {type: Boolean, 'default': false},
levelDrops: {type:Schema.Types.Mixed, 'default':{}},
chatRevoked: Boolean,
// Used to track the status of recapture emails sent to each user,
// can be 0 - no email sent - 1, 2 or 3 - 3 means no more email will be sent to the user
recaptureEmailsPhase: {type: Number, 'default': 0},
communityGuidelinesAccepted: {type: Boolean, 'default': false}
},
history: {
@ -274,7 +277,8 @@ var UserSchema = new Schema({
tagsCollapsed: {type: Boolean, 'default': false},
advancedCollapsed: {type: Boolean, 'default': false},
toolbarCollapsed: {type:Boolean, 'default':false},
background: String
background: String,
webhooks: {type: Schema.Types.Mixed, 'default': {}}
},
profile: {
blurb: String,
@ -322,6 +326,13 @@ var UserSchema = new Schema({
challenges: [{type: 'String', ref:'Challenge'}],
inbox: {
newMessages: {type:Number, 'default':0},
blocks: {type:Array, 'default':[]},
messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist
optOut: {type:Boolean, 'default':false}
},
habits: {type:[TaskSchemas.HabitSchema]},
dailys: {type:[TaskSchemas.DailySchema]},
todos: {type:[TaskSchemas.TodoSchema]},
@ -460,6 +471,10 @@ UserSchema.methods.unlink = function(options, cb) {
module.exports.schema = UserSchema;
module.exports.model = mongoose.model("User", UserSchema);
mongoose.model("User").find({$query:{'contributor.admin':true}, $orderby:{'contributor.level':-1, 'backer.npc':-1, 'profile.name':1}},function(err,mods){
module.exports.mods = mods
mongoose.model("User")
.find({'contributor.admin':true})
.sort('-contributor.level -backer.npc profile.name')
.select('profile contributor backer')
.exec(function(err,mods){
module.exports.mods = mods
});

View file

@ -10,6 +10,7 @@ $ mocha test/user.mocha.coffee
user = require("../controllers/user")
groups = require("../controllers/groups")
members = require("../controllers/members")
auth = require("../controllers/auth")
hall = require("../controllers/hall")
challenges = require("../controllers/challenges")
@ -320,7 +321,7 @@ module.exports = (swagger, v2) ->
parameters:[
body '','The array of batch-operations to perform','object'
]
middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron]
middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron, user.sessionPartyInvite]
action: user.batchUpdate
# Tags
@ -364,6 +365,46 @@ module.exports = (swagger, v2) ->
]
action: user.deleteTag
"/user/social/invite-friends":
spec:
method: 'POST'
description: 'Invite friends via email'
parameters: [
body 'invites','Array of [{name:"Friend\'s Name", email:"friends@email.com"}] to invite to play in your party','object'
]
action: user.inviteFriends
# Webhooks
"/user/webhooks":
spec:
method: 'POST'
description: 'Create a new webhook'
parameters: [
body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object'
]
action: user.addWebhook
"/user/webhooks/{id}:PUT":
spec:
path: '/user/webhooks/{id}'
method: 'PUT'
description: "Edit a webhook"
parameters: [
path 'id','The id of the webhook to edit','string'
body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object'
]
action: user.updateWebhook
"/user/webhooks/{id}:DELETE":
spec:
path: '/user/webhooks/{id}'
method: 'DELETE'
description: 'Delete a webhook'
parameters: [
path 'id','Id of webhook to delete','string'
]
action: user.deleteWebhook
# ---------------------------------
# Groups
# ---------------------------------
@ -539,9 +580,28 @@ module.exports = (swagger, v2) ->
# ---------------------------------
# Members
# ---------------------------------
"/members/{uid}":
"/members/{uuid}":
spec:{}
action: groups.getMember
action: members.getMember
"/members/{uuid}/message":
spec:
method: 'POST'
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'
]
middleware: [auth.auth]
action: members.sendPrivateMessage
"/members/{uuid}/block":
spec:
method: 'POST'
description: 'Block a member from sending private messages'
parameters: [
path 'uuid', 'The UUID of the member to message', 'string'
]
middleware: [auth.auth]
action: user.blockUser
# ---------------------------------
# Hall of Heroes / Patrons

View file

@ -7,13 +7,13 @@ var router = new express.Router();
auth.setupPassport(router); //FIXME make this consistent with the others
router.post('/api/v2/register', i18n.getUserLanguage, auth.registerUser);
router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
router.post('/api/v2/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook);
router.post('/api/v2/user/auth/social', i18n.getUserLanguage, auth.loginSocial);
router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword);
router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword);
router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername);
router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser);
router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
router.post('/api/v1/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook);
router.post('/api/v1/user/auth/social', i18n.getUserLanguage, auth.loginSocial);
module.exports = router;

View file

@ -3,11 +3,14 @@ var router = new express.Router();
var dataexport = require('../controllers/dataexport');
var auth = require('../controllers/auth');
var nconf = require('nconf');
var i18n = require('../i18n')
var i18n = require('../i18n');
var middleware = require('../middleware.js');
/* Data export */
router.get('/history.csv',auth.authWithSession,i18n.getUserLanguage,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes
router.get('/userdata.xml',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.xml);
router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json);
router.get('/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, dataexport.avatarPage);
router.get('/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, dataexport.avatarImage);
module.exports = router;

View file

@ -10,9 +10,8 @@ var isDev = nconf.get('NODE_ENV') === 'development';
if (cluster.isMaster && (isDev || isProd)) {
// Fork workers.
_.times(_.min([require('os').cpus().length,2]), function(){
cluster.fork();
});
var cpus = require('os').cpus();
_.times(nconf.get("HEROKU_PROD") ? (cpus.length-1 || 1) : (_.min([cpus.length,2])), cluster.fork);
cluster.on('disconnect', function(worker, code, signal) {
var w = cluster.fork(); // replace the dead worker
@ -26,6 +25,7 @@ if (cluster.isMaster && (isDev || isProd)) {
var path = require("path");
var swagger = require("swagger-node-express");
var autoinc = require('mongoose-id-autoinc');
var shared = require('habitrpg-shared');
// Setup translations
var i18n = require('./i18n');
@ -65,32 +65,23 @@ if (cluster.isMaster && (isDev || isProd)) {
// have a database of user records, the complete Facebook profile is serialized
// and deserialized.
passport.serializeUser(function(user, done) {
done(null, user);
done(null, user);
});
passport.deserializeUser(function(obj, done) {
done(null, obj);
done(null, obj);
});
// Use the FacebookStrategy within Passport.
// Strategies in Passport require a `verify` function, which accept
// credentials (in this case, an accessToken, refreshToken, and Facebook
// profile), and invoke a callback with a user object.
// FIXME
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
passport.use(new FacebookStrategy({
clientID: nconf.get("FACEBOOK_KEY"),
clientSecret: nconf.get("FACEBOOK_SECRET"),
callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
clientID: nconf.get("FACEBOOK_KEY"),
clientSecret: nconf.get("FACEBOOK_SECRET"),
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
},
function(accessToken, refreshToken, profile, done) {
// asynchronous verification, for effect...
//process.nextTick(function () {
// To keep the example simple, the user's Facebook profile is returned to
// represent the logged-in user. In a typical application, you would want
// to associate the Facebook account with a user record in your database,
// and return that user instead.
return done(null, profile);
//});
done(null, profile);
}
));

View file

@ -2,6 +2,7 @@ var nodemailer = require('nodemailer');
var nconf = require('nconf');
var crypto = require('crypto');
var path = require("path");
var request = require('request');
module.exports.ga = undefined; // set Google Analytics on nconf init
@ -21,6 +22,46 @@ module.exports.sendEmail = function(mailData) {
});
}
function getMailingInfo(user) {
var email, name;
if(user.auth.local && user.auth.local.email){
email = user.auth.local.email;
name = user.profile.name || user.auth.local.username;
}else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){
email = user.auth.facebook.emails[0].value;
name = user.auth.facebook.displayName || user.auth.facebook.username;
}
return {email: email, name: name};
}
module.exports.txnEmail = function(mailingInfo, emailType, variables){
if (mailingInfo._id) mailingInfo = getMailingInfo(mailingInfo);
if (!mailingInfo.email) return;
request({
url: nconf.get('EMAIL_SERVER:url') + '/job',
method: 'POST',
auth: {
user: nconf.get('EMAIL_SERVER:authUser'),
pass: nconf.get('EMAIL_SERVER:authPassword')
},
json: {
type: 'email',
data: {
emailType: emailType,
to: {
name: mailingInfo.name,
email: mailingInfo.email
},
variables: variables
},
options: {
attemps: 5,
backoff: {delay: 10*60*1000, type: 'fixed'}
}
}
});
}
// Encryption using http://dailyjs.com/2010/12/06/node-tutorial-5/
// Note: would use [password-hash](https://github.com/davidwood/node-password-hash), but we need to run
// model.query().equals(), so it's a PITA to work in their verify() function
@ -49,4 +90,19 @@ module.exports.setupConfig = function(){
require('newrelic');
module.exports.ga = require('universal-analytics')(nconf.get('GA_ID'));
};
};
var algorithm = 'aes-256-ctr';
module.exports.encrypt = function(text){
var cipher = crypto.createCipher(algorithm,nconf.get('SESSION_SECRET'))
var crypted = cipher.update(text,'utf8','hex')
crypted += cipher.final('hex');
return crypted;
}
module.exports.decrypt = function(text){
var decipher = crypto.createDecipher(algorithm,nconf.get('SESSION_SECRET'))
var dec = decipher.update(text,'hex','utf8')
dec += decipher.final('utf8');
return dec;
}

View file

@ -606,7 +606,7 @@ describe "API", ->
user = _user
done()
it.skip "Handles unsubscription", (done) ->
it "Handles unsubscription", (done) ->
cron = ->
user.lastCron = moment().subtract("d", 1)
user.fns.cron()

28
views/avatar-static.jade Normal file
View file

@ -0,0 +1,28 @@
doctype html
html(ng-app="habitrpg")
head
title=title
link(rel='shortcut icon', href='#{env.getBuildUrl("favicon.ico")}?v=3')
meta(charset='utf-8')
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='apple-mobile-web-app-capable', content='yes')
script(type='text/javascript').
window.env = !{JSON.stringify(env)};
!= env.getManifestFiles("app")
script(type='text/javascript').
window.habitrpg
.controller('StaticAvatarCtrl', ['$scope', function($scope){
$scope.profile = window.env.user;
}])
//webfonts
link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css')
body(ng-cloak)
include ./shared/header/avatar
div(ng-controller='StaticAvatarCtrl')
+herobox({main:true})

View file

@ -10,6 +10,9 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name='apple-mobile-web-app-capable', content='yes')
//FIXME for some reason this won't load when in footerCtrl.js#deferredScripts()
script(type="text/javascript", src="//s7.addthis.com/js/300/addthis_widget.js#pubid=ra-5016f6cc44ad68a4", async="async")
script(type='text/javascript').
window.env = !{JSON.stringify(env)};

View file

@ -11,7 +11,7 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('costumeText'))=env.t('costume')
.checkbox.equipment-title
label
input(type="checkbox", ng-model="user.preferences.costume", ng-click='set({"preferences.costume":!user.preferences.costume})')
input(type="checkbox", ng-model="user.preferences.costume", ng-change='set({"preferences.costume":user.preferences.costume ? true : false})')
|&nbsp;
=env.t('useCostume')
li.customize-menu(ng-if='user.preferences.costume')
@ -106,7 +106,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
| {{::egg.value}}
span.Pet_Currency_Gem1x.inline-gems
//- buyable quest eggs
each egg,quest in {gryphon:'Gryphon',hedgehog:'Hedgehog',ghost_stag:'Deer',rat:'Rat',octopus:'Octopus',dilatory_derby:'Seahorse',harpy:'Parrot',rooster:'Rooster',spider:'Spider'}
each egg,quest in {gryphon:'Gryphon',hedgehog:'Hedgehog',ghost_stag:'Deer',rat:'Rat',octopus:'Octopus',dilatory_derby:'Seahorse',harpy:'Parrot',rooster:'Rooster',spider:'Spider',owl:'Owl'}
div(ng-show='user.achievements.quests.#{quest} > 1')
button.customize-option(popover='{{::Content.eggs.#{egg}.notes()}}', popover-title!=env.t("egg", {eggType: "{{::Content.eggs.#{egg}.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.#{egg})', class='Pet_Egg_#{egg}')
p

View file

@ -54,8 +54,6 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html')
each t,k in env.Content.specialMounts
- var animal = k.split('-')[0], color = k.split('-')[1]
button(ng-if='user.items.mounts["#{animal}-#{color}"]', class="pet-button Mount_Head_#{animal}-#{color}", ng-class='{active: user.items.currentMount == "#{animal}-#{color}"}', ng-click='chooseMount("#{animal}", "#{color}")', popover=env.t(t), popover-trigger='mouseenter', popover-placement='bottom')
button(class="pet-button pet-not-owned", ng-hide='user.items.mounts["#{mount}"]')
.PixelPaw
script(type='text/ng-template', id='partials/options.inventory.pets.html')
.container-fluid
@ -99,7 +97,7 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html')
li.customize-menu
menu.pets-menu(label=env.t('food'))
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='left', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}')
button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='chooseFood(food)', class='Pet_Food_{{::food}}')
.badge.badge-info.stack-count {{points}}
// Remove this once we have images in
p {{:: Content.food[food].text()}}

View file

@ -4,19 +4,21 @@ mixin gemCost(cost)
= ' ' + env.t('locked')
block
-var showPath = function(path, items, joiner) { return path+'["'+items.join('"] '+joiner+' '+path+'["')+'"]'; }
-var unlockPath = function(path, items) { return 'unlock("'+path+'.'+items.join(','+path+'.')+'")'; }
// Make it a mixin so we can call it from mobile
mixin customizeProfile(mobile)
mixin buyPref(path,colors,title,status)
li.customize-menu(ng-if='#{status=="disabled" ? "user.purchased."+path+"."+colors.join(" || user.purchased."+path+".") : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "")
li.customize-menu(ng-if='#{status=="disabled" ? showPath("user.purchased."+path, colors, "||") : true}', class=~["limited","seasonal"].indexOf(status) ? "well limited-edition" : "")
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"} || user.purchased.#{path}.#{colors.join(" && user.purchased."+path+".")}', ng-click='unlock("#{path}.#{colors.join(","+path+".")}")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
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"/>'
each color in colors
button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}.#{color}}', ng-if='#{status!="disabled"} || user.purchased.#{path}.#{color}', ng-click='unlock("#{path}.#{color}")')
button.customize-option(type='button', class='#{path=="skin" ? "skin_"+color : "customize-option hair hair_bangs_1_"+color}', ng-class='{locked: !user.purchased.#{path}["#{color}"]}', ng-if='#{status!="disabled"} || user.purchased.#{path}["#{color}"]', ng-click='unlock("#{path}.#{color}")')
div(class=mobile ? 'padding' : 'container-fluid row')
.col-md-4
@ -35,9 +37,10 @@ mixin customizeProfile(mobile)
button.customize-option(class='{{user.preferences.size}}_shirt_'+shirt, type='button', ng-click='set({"preferences.shirt":"'+shirt+'"})')
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="user.purchased.shirt.convict && user.purchased.shirt.cross && user.purchased.shirt.fire && user.purchased.shirt.horizon && user.purchased.shirt.ocean && user.purchased.shirt.purple && user.purchased.shirt.rainbow && user.purchased.shirt.redblue && user.purchased.shirt.thunder && user.purchased.shirt.tropical && user.purchased.shirt.zombie", ng-click='unlock("shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each shirt in ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie']
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"/>'
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+'")')
@ -76,10 +79,11 @@ 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='user.purchased.hair.base.2 && user.purchased.hair.base.4 && user.purchased.hair.base.5 && user.purchased.hair.base.6 && user.purchased.hair.base.7 && user.purchased.hair.base.8', ng-click='unlock("hair.base.2,hair.base.4,hair.base.5,hair.base.6,hair.base.7,hair.base.8")')!= env.t('unlockSet',{cost:5}) + ' <span class="Pet_Currency_Gem1x inline-gems"/>'
each num in [2,4,5,6,7,8]
button(class='hair_base_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base.#{num}}', ng-click='unlock("hair.base.#{num}")')
button.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"/>'
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}")')
// Flower
li.customize-menu
@ -90,21 +94,20 @@ mixin customizeProfile(mobile)
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"/>'
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"/>'
// Beard
menu(label=env.t('beard'))
button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.beard":0})')
each num in [1,2,3]
button(class='hair_beard_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.beard.#{num}}', ng-click='unlock("hair.beard.#{num}")')
button(class='hair_beard_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.beard["#{num}"]}', ng-click='unlock("hair.beard.#{num}")')
// Mustache
menu(label=env.t('mustache'))
button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.mustache":0})')
each num in [1,2]
button(class='hair_mustache_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.mustache.#{num}}', ng-click='unlock("hair.mustache.#{num}")')
button(class='hair_mustache_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.mustache["#{num}"]}', ng-click='unlock("hair.mustache.#{num}")')
.col-md-4
h3(class=mobile?'item item-divider':'')=env.t('bodySkin')

View file

@ -63,6 +63,7 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
button.btn.btn-default(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour')
button.btn.btn-default(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey')
button.btn.btn-default(ng-click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal')
button.btn.btn-default(ng-click="openModal('invite-friends', {controller:'GroupsCtrl'})") Invite Friends
button.btn.btn-default(ng-if='user.preferences.disableClasses==true', ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass')
button.btn.btn-default(ng-if='!user.preferences.disableClasses && user.flags.classSelected', ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classTourPop'))= env.t('showClass')
@ -150,6 +151,33 @@ script(type='text/ng-template', id='partials/options.settings.api.html')
h6=env.t('qrCode')
img(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7B%22address%22%3A%22https%3A%2F%2Fhabitrpg.com%22%2C%22user%22%3A%22{{user.id}}%22%2C%22key%22%3A%22{{user.apiToken}}%22%7D&choe=UTF-8&chld=L', alt='qrcode')
hr
h2 Webhooks
table.table.table-striped
thead(ng-if='hasWebhooks')
tr
th Enabled
th Webhook URL
th
tbody
tr(ng-repeat="webhook in user.preferences.webhooks | toArray:true | orderBy:'sort'")
td
input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook.$key,webhook)')
td
input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook.$key,webhook)'}")
td
span.pull-left(ng-show='webhook._editing') *
a.checklist-icons(ng-click='deleteWebhook(webhook.$key)')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
tr
td(colspan=2)
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
.form-group.col-sm-10
input.form-control(type='url', ng-model='_newWebhook.url', placeholder='Webhook URL')
.col-sm-2
button.btn.btn-sm.btn-primary(type='submit') Add
script(id='partials/options.settings.export.html', type="text/ng-template")
.container-fluid
.row
@ -194,25 +222,21 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template')
label
script(id='partials/options.settings.subscription.html',type='text/ng-template')
.well
h2=env.t('individualSub')
div(ng-if='!user.purchased.plan.customerId')
div(ng-include="'partials/options.settings.subscription.perks.html'")
p
small.muted Payment Methods:
.btn.btn-primary(ng-click='showStripe(true)') Card
//a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal
div(ng-if='user.purchased.plan.customerId')
p.alert.alert-warning(ng-if='user.purchased.plan.dateTerminated')
i.glyphicon.glyphicon-time
| &nbsp;
=env.t('subCanceled')
| <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
p.lead
=env.t('subscribed')
| &nbsp;
span.glyphicon.glyphicon-ok
div(ng-include="'partials/options.settings.subscription.perks.html'")
.btn.btn-primary(ng-if=':: !user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card
.btn.btn-sm.btn-danger(ng-if=':: !user.purchased.plan.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub')
.well(ng-init="p = user.purchased.plan")
div(ng-if='p.customerId')
p.alert.alert-warning(ng-if='p.dateTerminated')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(p.dateTerminated).format('MM/DD/YYYY')}}</strong>
p.alert.alert-success(ng-if='!p.dateTerminated')=env.t('subscribed')
h2=env.t('individualSub')
div(ng-include="'partials/options.settings.subscription.perks.html'")
div(ng-if='!p.customerId || (p.customerId && p.dateTerminated)')
h3(ng-if='(p.customerId && p.dateTerminated)') Resubscribe
a.btn.btn-primary(ng-click='showStripe(true)') Card
//a.btn.btn-warning(ng-click='paypalSubscribe()') PayPal
a.btn.btn-warning(href='/paypal/subscribe?_id={{user._id}}&apiToken={{user.apiToken}}') PayPal
div(ng-if='p.customerId')
.btn.btn-primary(ng-if='!p.dateTerminated && p.paymentMethod=="Stripe"', ng-click='showStripeEdit()') Update Card
.btn.btn-sm.btn-danger(ng-if='!p.dateTerminated', ng-click='cancelSubscription()')=env.t('cancelSub')

View file

@ -1,18 +1,16 @@
//div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted')
// p If you would like to post messages in the Tavern or any party or guild chat, please first read our
// |&nbsp;
// a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
// |&nbsp;and then click the button below to indicate that you accept them.
// .chat-controls
// div
// button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
// .chat-buttons
// button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg'))
// span.glyphicon.glyphicon-refresh
div.chat-form.guidelines-not-accepted(ng-if='!user.flags.communityGuidelinesAccepted')
p If you would like to post messages in the Tavern or any party or guild chat, please first read our
|&nbsp;
a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
|&nbsp;and then click the button below to indicate that you accept them.
.chat-controls
div
button.btn.btn-warning(ng-click='acceptCommunityGuidelines()')=env.t('iAcceptCommunityGuidelines')
.chat-buttons
button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg'))
span.glyphicon.glyphicon-refresh
//form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)')
//////////// When we want to block the ability to chat until the Community Guidelines have been accepted, delete the one line immediately below this comment and un-comment all lines above.
form.chat-form(ng-submit='postChat(group,message.content)')
form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postChat(group,message.content)')
div(ng-controller='AutocompleteCtrl')
textarea.form-control(rows=4, ui-keypress='{13:"postChat(group,message.content)"}', ng-model='message.content', updateinterval='250', flag='@', at-user, auto-complete)
span.user-list(ng-show='!isAtListHidden')
@ -24,5 +22,4 @@ form.chat-form(ng-submit='postChat(group,message.content)')
include ../../shared/formatting-help
.chat-buttons
input(type='submit', value=env.t('sendChat'), ng-class='{disabled: _sending == true}')
button(type="button", ng-click='sync(group)', tooltip=env.t('toolTipMsg'))
span.glyphicon.glyphicon-refresh
button(type="button", ng-click='sync(group)')=env.t('toolTipMsg')

View file

@ -1,19 +1,32 @@
li.chat-message(ng-repeat='message in group.chat track by message.id', ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}')
.scrollable-message
span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label
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')
span
a.label.label-default(ng-show='countExists(message.likes)', ng-class='{"label-success":message.likes[user._id]}', ng-click='likeChatMessage(group,message)') +{{countExists(message.likes)}}
a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group,message)') +1
| &nbsp;
a(ng-if=':: user.contributor.admin || message.uuid == user.id', ng-click='deleteChatMessage(group, message)')
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
a.label.label-default.chat-message(ng-if=':: message.user', class='float-label', ng-class='userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}&nbsp;
span(ng-class='userAdminGlyphiconStyleFromLevel(message.contributor.level)')
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
span(ng-if='::message.user')
a.label.label-default.chat-message.hidden-label
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
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')
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)}}
a.chat-plus-one.muted(ng-show='!countExists(message.likes)', ng-click='likeChatMessage(group, message)') +1
| &nbsp; &nbsp;
a(ng-click="quickReply(message.uuid)")
span.glyphicon.glyphicon-envelope(tooltip=env.t('sendPM'))
if inbox
a(ng-click="quickReply(message.uuid)")
span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply'))
| &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
a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)')
span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent')
span(tooltip='{{::contribText(message.contributor, message.backer)}}') {{::message.user}}&nbsp;
span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)')

View file

@ -48,9 +48,11 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
// ------ Members -------
.panel.panel-default
.panel-heading
h3.panel-title=env.t('members')
h3.panel-title
=env.t('members')
button.pull-right.btn.btn-primary(ng-click="openModal('invite-friends', {controller:'GroupsCtrl'})", ng-if='::group.type=="party"') Invite Friends
.panel-body.modal-fixed-height
div(ng-if='group.type=="party"')
div(ng-if='::group.type=="party"')
=env.t('partyList')
br
select#partyOrder(
@ -86,7 +88,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
a.media-body
span(ng-click='clickMember(invite._id, true)')
| {{invite.profile.name}}
.panel-footer
.panel-footer(ng-if='::group.type!="party"')
form.form-inline(ng-submit='invite(group)')
//.alert.alert-danger(ng-show='_groupError') {{_groupError}}
.form-group
@ -112,5 +114,4 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
h3=env.t('chat')
include ./chat-box
ul.list-unstyled.tavern-chat
include ./chat-message
+chatMessages()

View file

@ -1,9 +1,22 @@
// FIXME note, due to https://github.com/angular-ui/bootstrap/issues/783 we can't use nested angular-bootstrap tabs
// Subscribe to that ticket & change this when they fix
include ./challenges.jade
include ./hall.jade
include ./boss.jade
include ./challenges
include ./hall
include ./boss
include ./chat-message
script(type='text/ng-template', id='partials/options.social.inbox.html')
.container-fluid
.row
.col-md-12
+chatMessages('inbox')
.form-inline
a.btn.btn-xs.btn-danger(popover=env.t('clearAllPopover'), popover-trigger='mouseenter', ng-click='user.ops.clearPMs({})', popover-placement='right')=env.t('clearAll')
.checkbox
label
input(type='checkbox', ng-model='user.inbox.optOut', ng-change='set({"inbox.optOut": user.inbox.optOut?true: false})')
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')
include ./tavern
@ -78,6 +91,10 @@ script(type='text/ng-template', id='partials/options.social.guilds.html')
script(type='text/ng-template', id='partials/options.social.html')
ul.options-menu
li(ng-class="{ active: $state.includes('options.social.inbox') }")
a(ui-sref='options.social.inbox')
| Inbox&nbsp;
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li(ng-class="{ active: $state.includes('options.social.tavern') }")
a(ui-sref='options.social.tavern')
=env.t('tavern')

View file

@ -149,10 +149,10 @@
!= ' ' + env.t('tavernAlert1') + ' <a href="https://github.com/HabitRPG/habitrpg/issues/2760" target="_blank">' + env.t('tavernAlert2') + '</a>.<br />' + env.t('moderatorIntro1')
span(ng-repeat='mod in env.mods')
|&nbsp; &nbsp;
span(ng-if='mod.contributor.admin',popover=env.t('gamemaster'),popover-trigger='mouseenter',popover-placement='right')
a.label.label-default(ng-class='userLevelStyle(mod)', ng-click='clickMember(mod._id, true)')
{{mod.profile.name}}&nbsp;
span(ng-class='userAdminGlyphiconStyle(mod)')
span(ng-if='::mod.contributor.admin',popover=env.t('gamemaster'),popover-trigger='mouseenter',popover-placement='right')
a.label.label-default(ng-class='::userLevelStyle(mod)', ng-click='clickMember(mod._id, true)')
{{::mod.profile.name}}&nbsp;
span(ng-class='::userAdminGlyphiconStyle(mod)')
p
=env.t('communityGuidelinesRead1')
| &nbsp;
@ -160,5 +160,4 @@
| &nbsp;
=env.t('communityGuidelinesRead2')
ul.list-unstyled.tavern-chat
include ./chat-message
+chatMessages()

View file

@ -7,6 +7,8 @@ mixin avatar(opts)
.character-sprites
.addthis_native_toolbox(ng-if='profile._id==user._id', data-url="#{env.BASE_URL}/static/front/#?memberId={{profile._id}}", data-title="Check out my HabitRPG progress!")
// Mount Body
if !opts.minimal
span(ng-if='profile.items.currentMount', class='Mount_Body_{{profile.items.currentMount}}')

View file

@ -24,6 +24,10 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li
a(ui-sref='options.profile.profile')=env.t('profile')
ul.toolbar-submenu
li
a(ui-sref='options.social.inbox')
| Inbox&nbsp;
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li
a(ui-sref='options.social.tavern')=env.t('tavern')
li
@ -43,6 +47,11 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
a(ui-sref='options.inventory.mounts')=env.t('mounts')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
ul.toolbar-submenu
li
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
li
a(ui-sref='options.settings.export')=env.t('exportData')
ul.toolbar-submenu
li
a(target="_blank" href='http://habitrpg.wikia.com/wiki/FAQ')=env.t('FAQ')
@ -83,10 +92,15 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
li.toolbar-button-dropdown
a(ui-sref='options.social.tavern')
span=env.t('social')
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
a(ng-click='expandMenu("social")', ng-class='{active: _expandedMenu == "social"}')
span &#9776;
div(ng-if='_expandedMenu == "social"')
ul.toolbar-submenu(ng-click='expandMenu(null)')
li
a(ui-sref='options.social.inbox')
| Inbox&nbsp;
span.badge.badge-danger(ng-if='user.inbox.newMessages') {{user.inbox.newMessages}}
li
a(ui-sref='options.social.tavern')=env.t('tavern')
li
@ -112,6 +126,17 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
a(ui-sref='options.inventory.mounts')=env.t('mounts')
li
a(ui-sref='options.inventory.equipment')=env.t('equipment')
li.toolbar-button-dropdown
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')
span=env.t('data')
a(ng-click='expandMenu("data")', ng-class='{active: _expandedMenu == "data"}')
span &#9776;
div(ng-if='_expandedMenu == "data"')
ul.toolbar-submenu(ng-click='expandMenu(null)')
li
a(target="_blank" ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
li
a(ui-sref='options.settings.export')=env.t('exportData')
li.toolbar-button-dropdown
a(target="_blank" href='http://habitrpg.wikia.com/wiki/')
span=env.t('help')
@ -131,6 +156,8 @@ nav.toolbar(ng-controller='AuthCtrl', ng-class='{active: isToolbarHidden}')
a(target="_blank" href='http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG')=env.t('contributeToHRPG')
li
a(target="_blank" href='http://habitrpg.wikia.com/wiki/')=env.t('overview')
li(ng-controller='SettingsCtrl')
a(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour')
ul.toolbar-subscribe(ng-if='!user.purchased.plan.customerId')
li.toolbar-subscribe-button
button(ui-sref='options.settings.subscription',popover-trigger='mouseenter',popover-placement='bottom',popover-title=env.t('subscriptions'),popover=env.t('subDescription'),popover-append-to-body='true')=env.t('subscribe')

View file

@ -10,3 +10,4 @@ include ./classes
include ./quests
include ./rebirth
include ./limited
include ./invite-friends

View file

@ -0,0 +1,48 @@
script(type='text/ng-template', id='modals/invite-friends.html')
.modal-header
h4 Invite Friends
.modal-body
p.alert.alert-info Invite friends by <a href='http://habitrpg.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.
form.form-inline(ng-submit='invite(party)')
//-.alert.alert-danger(ng-show='_groupError') {{_groupError}}
.form-group
input.form-control(type='text', placeholder=env.t('userId'), ng-model='party.invitee')
|&nbsp;
button.btn.btn-primary(type='submit') Invite Existing User
hr
p.alert.alert-info Invite friends by email. If they join via your email, they'll automatically be invited to your party.
form.form-horizontal(ng-submit='inviteEmails(inviter, emails)')
table.table.table-striped
thead
tr
th Name
th Email
tbody
tr(ng-repeat='email in emails')
td
input.form-control(type='text', ng-model='email.name')
td
input.form-control(type='email', ng-model='email.email')
tr
td(colspan=2)
a.btn.btn-xs.pull-right(ng-click='emails = emails.concat([{name:"",email:""}])')
i.glyphicon.glyphicon-plus
tr
td.form-group(colspan=2)
label.col-sm-1.control-label By:
.col-sm-7
input.form-control(type='text', ng-model='inviter')
.col-sm-4
button.btn.btn-primary(type='submit') Invite New User(s)
//-
hr
p.alert.alert-info Or share this link (copy/paste):
input.form-control(type='text', ng-value='inviteLink({id: party._id, inviter: user._id, name: party.name})')
.modal-footer
button.btn.btn-default(ng-click='$close()') Close

View file

@ -1,32 +1,50 @@
script(type='text/ng-template', id='modals/member.html')
.modal-header(bindonce='profile')
script(type='text/ng-template', id='modals/member.html')
.modal-header
h4
span {{profile.profile.name}}
span(ng-if='profile.contributor.level') - {{contribText(profile.contributor, profile.backer)}}
.modal-body(bindonce='profile')
span {{::profile.profile.name}}
span(ng-if='profile.contributor.level') - {{::contribText(profile.contributor, profile.backer)}}
.modal-body
.container-fluid
.row
.col-md-6
img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}')
markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb')
ul.muted.list-unstyled(ng-if='profile.auth.timestamps')
img(ng-show='::profile.profile.imageUrl', ng-src='{{::profile.profile.imageUrl}}')
markdown(ng-show='::profile.profile.blurb', ng-model='::profile.profile.blurb')
ul.muted.list-unstyled(ng-if='::profile.auth.timestamps')
li {{profile._id}}
li(ng-show='profile.auth.timestamps.created')
li(ng-show='::profile.auth.timestamps.created')
|&nbsp;
=env.t('memberSince')
|&nbsp;
| {{timestamp(profile.auth.timestamps.created)}} -
li(ng-show='profile.auth.timestamps.loggedin')
| {{::timestamp(profile.auth.timestamps.created)}} -
li(ng-show='::profile.auth.timestamps.loggedin')
|&nbsp;
=env.t('lastLoggedIn')
|&nbsp;
| {{timestamp(profile.auth.timestamps.loggedin)}} -
| {{::timestamp(profile.auth.timestamps.loggedin)}} -
h3=env.t('stats')
.label.label-info {{ {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
.label.label-info {{:: {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[profile.stats.class] }}
include ../profiles/stats
.col-md-6
+herobox()
h3=env.t('achievements')
include ../profiles/achievements
.row
+herobox()
.row
h3=env.t('achievements')
include ../profiles/achievements
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('ok')
.btn-group.pull-left(ng-if='::user')
button.btn.btn-md.btn-default(ng-show='user.inbox.blocks | contains:profile._id', tooltip=env.t('unblock'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
span.glyphicon.glyphicon-plus
button.btn.btn-md.btn-default(ng-hide='user.inbox.blocks | contains:profile._id', tooltip=env.t('block'), ng-click="user.ops.blockUser({params:{uuid:profile._id}})", tooltip-placement='right')
span.glyphicon.glyphicon-ban-circle
button.btn.btn-md.btn-default(tooltip=env.t('sendPM'), ng-click="openModal('private-message',{controller:'MemberModalCtrl'})", tooltip-placement='right')
span.glyphicon.glyphicon-envelope
button.btn.btn-default(ng-click='$close()')=env.t('close')
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')
.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')

View file

@ -8,14 +8,113 @@ table
h3.popover-title
a(target='_blank', href='https://twitter.com/Mihakuu') Bailey
.popover-content
h5 HAPPY THANKSGIVING!
table.table.table-striped
tr
td
h5 November Mystery Item Set
.pull-right.inventory_present
p Cool! What could it be? All Habiticans who are subscribed during the month of November will receive the November Mystery Item Set! It will be revealed on the 25th, so keep your eyes peeled. Thanks for supporting the site <3
p.small.muted 12/01/2014
h5 Happy Thanksgiving!
p It's Thanksgiving in Habitica! On this day Habiticans celebrate by spending time with loved ones, giving thanks, and riding their glorious turkeys into the magnificent sunset. Some of the NPCs are celebrating the occasion!
p.small.muted by Lemoness
tr
td
h5 Turkey Pet and Mount!
p Those of you who weren't around last Thanksgiving have received an adorable Turkey Pet, and those of you who got a Turkey Pet last year have received a handsome Turkey Mount! Thank you for using HabitRPG - we really love you guys <3
p.small.muted by Lemoness
p.small.muted 11/26/2014
h5 11/25/2014
table.table.table-striped
tr
td
h5 November Item Set Revealed
p The November Subscriber Item has been revealed: the Feast and Fun Set! All November subscribers will receive the Pitchfork of Feasting and the Steel Helm of Sporting. You still have five days to subscribe 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 Private Messaging Version 1.0
p We're excited to announce a new feature: Private Messaging! Now you can send someone a PM by clicking the envelope icon in the bottom-left of their profile window . You can check your messages under Social > Inbox! This is a very rudimentary feature so far, only containing the ability to send messages, block people, and opt out. To read about some of the planned features for the future and make suggestions, check out <a href='https://trello.com/c/hHpIzMc5/459-private-messaging-v-2' target='_blank'>this Trello card</a>!
p.small.muted by Lefnire
h5 11/18/2014
table.table.table-striped
tr
td
h5 New Pet Quest: The Night-Owl!
p Habiticans are in the dark when a giant Night-Owl blots out the Tavern light! Can you drive it away in time to finish your all-nighter? If so, you may find some cute pet owls in the morning...
p.small.muted by Twitching, Lemoness, and Arcosine
h5 11/13/2014 - Share Avatar To Social Media, Email Invites, First Mini Quest, And Data Tab
table.table.table-striped
tr
td
h5 Share Avatar To Social Media
p You can now automatically share your avatar and public profile to social media! Just hover over the picture and click the "Share" button in the right-hand corner. Show off your outfit, your achievements, and your profile picture! Note that your tasks, as always, remain 100% private.
p.small.muted by Lefnire
tr
td
h5 Invite Friends To Party Via Email
p Do you want to invite friends to join your party without inputting their User ID? Now you can send them an email directly from the party page - even if they don't have an account yet!
p.small.muted by Lefnire
tr
td
h5 Mini Quest: The Basi-List!
p Now when someone accepts your party invitation and joins your party, you will be given a Mini Quest: The Basi-List! Battle the Basi-List with your friends for an XP and GP reward.
p.small.muted by Arcosine and Redphoenix
tr
td
h5 Data Tab
p Now you can access the Data Display Tool and Export Data from the toolbar!
p.small.muted by ShilohT
h5 11/12/2014
table.table.table-striped
tr
td
h5 New Equipment Quest Line: The Golden Knight!
p The Golden Knight believes that she is the perfect Habitican, and that anyone who slips up in their quest for self-improvement is a lazy failure. Can you talk some sense into her - or will it come to blows? If you complete the entire quest line, you'll be rewarded with a legendary weapon...
p The first scroll in this quest line, "A Stern Talking-to," drops automatically at Level 40! If you're already over Level 40, you will automatically be awarded this quest - just check off a task and then check your inventory.
h5 11/09/2014 - Facebook Login Fixed For Mobile And Community Guidelines To Chat
table.table.table-striped
tr
td
h5 Facebook Login Fixed For Mobile!
p Great news! If you use Facebook to log in to the mobile app, we've released an update so you no longer have to type in your UUID/API manually, misspelling things on your tiny keyboard and bemoaning your fate. Thank goodness! The Android update is out now, and the iOS update has been submitted and should be out soon.
tr
td
h5 Community Guidelines To Chat
p Before you can use any of the public chat features, you now have to agree to our Community Guidelines. We know they're long, but they're important, so please do read them if you haven't already. Plus, we worked hard to make them entertaining, and they were illustrated by many of our excellent artisans!
h5 11/06/2014
table.table.table-striped
tr
td
h5 Bailey: Costume Challenge Badges Awarded!
p The HabitRPG Costume Challenge Badges have been awarded! Thanks for your patience while we went through all the entries individually. You can see some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the HabitRPG blog</a> already, and more will be added every week.
p IMPORTANT: some of the links that people provided did not work. If you entered the Challenge but even after refreshing the page you still don't have your badge, email leslie@habitrpg.com with the link to your costume and your avatar. (The costume and avatar must have been posted prior to November 1st to count.)
p Thanks to all our amazing participants!
h5 11/05/2014- November Backgrounds And Beeminder Integration
table.table.table-striped
tr
td
h5 November Backgrounds
p There are three new avatar backgrounds in the <a href='https://habitrpg.com/#/options/profile/backgrounds' target='_blank'>Background Shop</a>! Now your avatar can enjoy a Harvest Feast, admire a Sunset Meadow, or gaze at the Starry Skies!
p.small.muted by Kiwibot, Holsety1, and Draayder
tr
td
h5 Beeminder Integration
p We've integrated with Beeminder! Now you can beemind your To-Dos automatically :) <a href='https://www.beeminder.com/habitrpg' target='_blank'>Check it out</a>!
p If you've never heard of Beeminder or want to learn more about what we've integrated so far, check out our <a href='http://blog.habitrpg.com/post/101773418876/beeminder-integration' target='_blank'>blog post about it</a>. Enjoy!
p.small.muted by Alys and Alice Monday
h5 11/01/2014
table.table.table-striped
tr
td
h5 November Mystery Item Set
.pull-right.inventory_present
p Cool! What could it be? All Habiticans who are subscribed during the month of November will receive the November Mystery Item Set! It will be revealed on the 25th, so keep your eyes peeled. Thanks for supporting the site <3
h5 10/31/2014 - Monster Npcs, Last Day For Fall Festival Items, Last Day Of Community Costume Challenge, Last Day For Winged Goblin Item Set
table.table.table-striped

View file

@ -4,11 +4,11 @@
script(id='templates/habitrpg-tasks.html', type="text/ng-template")
.tasks-lists.container-fluid
.row
.col-md-3.col-sm-6(bindonce='lists', ng-repeat='list in lists', bo-class='{"rewards-module": list.type==="reward"}')
.col-md-3.col-sm-6(bindonce='lists', ng-repeat='list in lists', ng-class='::{"rewards-module": list.type==="reward"}')
.task-column(class='{{list.type}}s')
// Todos export/graph options
span.option-box.pull-right(bo-if='main && list.type=="todo"')
span.option-box.pull-right(ng-if='::main && list.type=="todo"')
a.option-action(ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip=env.t('progress'))
span.glyphicon.glyphicon-signal
//a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal')
@ -20,19 +20,18 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
h2.task-column_title {{list.header}}
// Todo Chart
.todos-chart(bo-if='list.type == "todo"', ng-show='charts.todos')
.todos-chart(ng-if='::list.type == "todo"', ng-show='charts.todos')
// Add New
form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-hide='obj._locked || (list.showCompleted && list.type=="todo")', ng-submit='addTask(obj[list.type+"s"],list)')
span.addtask-field
input(type='text', ng-model='list.newTask', placeholder='{{list.placeHolder}}', required)
input.addtask-btn(type='submit', value='', ng-disabled='new{{list.type}}form.$invalid')
hr
form.task-add(name='new{{list.type}}form', ng-hide='obj._locked || (list.showCompleted && list.type=="todo")', ng-submit='addTask(obj[list.type+"s"],list)')
input(type='text', ng-model='list.newTask', placeholder='{{list.placeHolder}}', required)
button(type='submit', ng-disabled='new{{list.type}}form.$invalid')
span.glyphicon.glyphicon-plus
mixin taskColumnTabs(position)
// Habits Tabs
div(bo-if='main && list.type=="habit"', class='tabbable tabs-below')
ul.nav.nav-tabs
div(ng-if='::main && list.type=="habit"', class='tabbable tabs-below')
ul.task-filter
li(ng-class='{active: list.view == "all"}')
a(ng-click='list.view = "all"')=env.t('all')
li(ng-class='{active: list.view == "yellowred"}')
@ -40,9 +39,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
li(ng-class='{active: list.view == "greenblue"}')
a(ng-click='list.view = "greenblue"')=env.t('greenblue')
// Daily Tabs
div(bo-if='main && list.type=="daily"', class='tabbable tabs-below')
div(ng-if='::main && list.type=="daily"', class='tabbable tabs-below')
// remaining/completed tabs
ul.nav.nav-tabs
ul.task-filter
li(ng-class='{active: list.view == "all"}')
a(ng-click='list.view = "all"')=env.t('all')
li(ng-class='{active: list.view == "remaining"}')
@ -50,7 +49,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
li(ng-class='{active: list.view == "complete"}')
a(ng-click='list.view = "complete"')=env.t('grey')
// Todo Tabs
div(bo-if='main && list.type=="todo"', bo-class='{"tabbable tabs-below": list.type=="todo"}')
div(ng-if='::main && list.type=="todo"', ng-class='::{"tabbable tabs-below": list.type=="todo"}')
// div(ng-show='list.view == "complete" || list.view == "all"')
// li.task.reward-item(ng-if='#{canceler ? "user.stats.buffs."+canceler : "user.items.special."+k+">0"}',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.#{k}.notes()}}')
if position=="bottom"
@ -58,15 +57,16 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
.alert
=env.t('lotOfToDos')
button.task-action-btn.tile.spacious.bright(ng-click='user.ops.clearCompleted({})',popover=env.t('deleteToDosExplanation'),popover-trigger='mouseenter')=env.t('clearCompleted')
p!=env.t('beeminderDeleteWarning')
// remaining/completed tabs
ul.nav.nav-tabs
ul.task-filter
li(ng-class='{active: !list.showCompleted}')
a(ng-click='list.showCompleted = false')=env.t('remaining')
li(ng-class='{active: list.showCompleted}')
a(ng-click='list.showCompleted= true')=env.t('complete')
// Rewards Tabs
div(bo-if='main && list.type=="reward"', class='tabbable tabs-below')
ul.nav.nav-tabs
div(ng-if='::main && list.type=="reward"', class='tabbable tabs-below')
ul.task-filter
li(ng-class='{active: list.view == "all"}')
a(ng-click='list.view = "all"')=env.t('all')
li(ng-class='{active: list.view == "ingamerewards"}')
@ -91,7 +91,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.reward-cost {{item.value}}
span.shop_gold
// main content
span(bo-class='{"shop_{{item.key}} shop-sprite item-img": true}').reward-img
span(ng-class='::{"shop_{{item.key}} shop-sprite item-img": true}').reward-img
p.task-text {{item.text()}}
// Events
@ -139,7 +139,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
br
// Ads
div(bo-if='main && !user.purchased.ads && !user.purchased.plan.customerId && list.type!="reward"')
div(ng-if='::main && !user.purchased.ads && !user.purchased.plan.customerId && list.type!="reward"')
span.pull-right
a(ui-sref='options.settings.subscription', popover=env.t('removeAds'), popover-trigger='mouseenter')
span.glyphicon.glyphicon-remove

View file

@ -1,4 +1,4 @@
li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", data-popover-placement="top", ng-show='shouldShow(task, list, user.preferences)')
li(bindonce='list', id='task-{{::task.id}}', ng-repeat='task in obj[list.type+"s"]', class='task {{Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)', ng-class='{"cast-target":spell && (list.type != "reward")}', popover-trigger='mouseenter', data-popover-html="{{task.notes | markdown}}", data-popover-placement="top", ng-show='shouldShow(task, list, user.preferences)')
// right-hand side control buttons
.task-meta-controls
@ -60,7 +60,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s
.task-controls.task-primary(ng-if='!task._editing')
// Habits
span(bo-if='task.type=="habit"')
span(ng-if='::task.type=="habit"')
// score() is overridden in challengesCtrl to do nothing
a.task-action-btn(ng-if='task.up', ng-click='score(task,"up")') +
a.task-action-btn(ng-if='task.down', ng-click='score(task,"down")') -
@ -72,7 +72,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s
span.shop_gold
// Daily & Todos
span.task-checker.action-yesno(bo-if='task.type=="daily" || task.type=="todo"')
span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"')
input.visuallyhidden.focusable(ng-if='$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox', ng-model='task.completed', ng-change='task.type=="todo" && pushTask(task,$index,"bottom"); changeCheck(task)')
input.visuallyhidden.focusable(ng-if='!$state.includes("tasks")', id='box-{{obj._id}}_{{task.id}}', type='checkbox')
label(for='box-{{obj._id}}_{{task.id}}')
@ -163,17 +163,17 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s
label(for='{{obj._id}}_{{task.id}}-option-minus')
// if Daily, calendar
fieldset(bo-if='task.type=="daily"', class="option-group")
fieldset(ng-if='::task.type=="daily"', class="option-group")
legend.option-title=env.t('repeat')
.task-controls.tile-group.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)', bo-text='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)', bo-text='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)', bo-text='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)', bo-text='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)', bo-text='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)', bo-text='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)', bo-text='moment.weekdaysMin(6)')
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)}}
// if Reward, pricing
fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id')
@ -185,7 +185,7 @@ li(bindonce='list', bo-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')
input.option-content.datepicker(type='text', datepicker-popup='MM/dd/yyyy', ng-model='task.date', is-open='datepickerOpened', ng-click='datepickerOpened = true')
// Tags
fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
@ -195,7 +195,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s
markdown(ng-model='tag.name')
// Advanced Options
span(bo-if='task.type!="reward"')
span(ng-if='::task.type!="reward"')
p.option-title.mega(ng-click='task._advanced = !task._advanced', tooltip=env.t('expandCollapse'))=env.t('advancedOptions')
fieldset.option-group.advanced-option(ng-class="{visuallyhidden: task._advanced}")
legend.option-title

View file

@ -42,22 +42,10 @@ block content
div(class='clearfix')
img(class='pull-right', src='/community-guidelines-images/beingHabitican.png', alt='')
ul
li
strong=env.t('commGuideList01A')
|&nbsp;
=env.t('commGuideList01Apart2')
li
strong=env.t('commGuideList01B')
|&nbsp;
=env.t('commGuideList01Bpart2')
li
strong=env.t('commGuideList01C')
|&nbsp;
=env.t('commGuideList01Cpart2')
li
strong=env.t('commGuideList01D')
|&nbsp;
=env.t('commGuideList01Dpart2')
li!=env.t('commGuideList01A')
li!=env.t('commGuideList01B')
li!=env.t('commGuideList01C')
li!=env.t('commGuideList01D')
h2=env.t('commGuideHeadingMeet')
p=env.t('commGuidePara006')
@ -109,12 +97,7 @@ block content
strong Megan
li
strong Daniel the Bard
p=env.t('commGuidePara012')
|&nbsp;(
a(href='mailto:leslie@habitrpg.com') leslie&commat;habitrpg&period;com
|).
p!=env.t('commGuidePara012')
p=env.t('commGuidePara013')
p=env.t('commGuidePara014')
|&nbsp;
@ -124,49 +107,17 @@ block content
img(class='pull-right', src='/community-guidelines-images/publicSpaces.png', alt='')
p=env.t('commGuidePara015')
p=env.t('commGuidePara016')
p
strong=env.t('commGuidePara017')
|&nbsp;
=env.t('commGuidePara017part2')
p!=env.t('commGuidePara017')
ul
li
strong=env.t('commGuideList02A')
li
strong=env.t('commGuideList02B')
|&nbsp;
=env.t('commGuideList02Bpart2')
li
strong=env.t('commGuideList02C')
|&nbsp;
=env.t('commGuideList02Cpart2')
li
strong=env.t('commGuideList02D')
|&nbsp;
=env.t('commGuideList02Dpart2')
li
strong=env.t('commGuideList02E')
|&nbsp;
=env.t('commGuideList02Epart2')
li
strong=env.t('commGuideList02F')
|&nbsp;
=env.t('commGuideList02Fpart2')
li
strong=env.t('commGuideList02G')
|&nbsp;
=env.t('commGuideList02Gpart2')
li
strong=env.t('commGuideList02H')
|&nbsp;
=env.t('commGuideList02Hpart2')
|&nbsp;
a(href='mailto:leslie@habitrpg.com') leslie&commat;habitrpg&period;com
|&nbsp;
=env.t('commGuideList02Hpart3')
p
strong=env.t('commGuidePara019')
|&nbsp;
=env.t('commGuidePara019part2')
li!=env.t('commGuideList02A')
li!=env.t('commGuideList02B')
li!=env.t('commGuideList02C')
li!=env.t('commGuideList02D')
li!=env.t('commGuideList02E')
li!=env.t('commGuideList02F')
li!=env.t('commGuideList02G')
li!=env.t('commGuideList02H')
p!=env.t('commGuidePara019')
p=env.t('commGuidePara021')
h3=env.t('commGuideHeadingTavern')
@ -175,112 +126,43 @@ block content
p=env.t('commGuidePara022')
p
strong=env.t('commGuidePara023')
p=env.t('commGuidePara024')
|&nbsp;
strong=env.t('commGuidePara024part2')
|&nbsp;
=env.t('commGuidePara024part3')
p
strong=env.t('commGuidePara027')
|&nbsp;
=env.t('commGuidePara027part2')
p!=env.t('commGuidePara024')
p!=env.t('commGuidePara027')
h3=env.t('commGuideHeadingPublicGuilds')
div(class='clearfix')
img(class='pull-right', src='/community-guidelines-images/publicGuilds.png', alt='')
p
strong=env.t('commGuidePara029')
|&nbsp;
=env.t('commGuidePara029part2')
|&nbsp;
strong=env.t('commGuidePara029part3')
p
strong=env.t('commGuidePara031')
|&nbsp;
=env.t('commGuidePara031part2')
p
strong=env.t('commGuidePara033')
|&nbsp;
=env.t('commGuidePara033part2')
|&nbsp;
a(href='mailto:leslie@habitrpg.com') leslie&commat;habitrpg&period;com
|&nbsp;
=env.t('commGuidePara033part3')
p
strong=env.t('commGuidePara035')
|&nbsp;
=env.t('commGuidePara035part2')
p!=env.t('commGuidePara029')
p!=env.t('commGuidePara031')
p!=env.t('commGuidePara033')
p!=env.t('commGuidePara035')
p
strong=env.t('commGuidePara037')
h3=env.t('commGuideHeadingBackCorner')
div(class='clearfix')
img(class='pull-left', src='/community-guidelines-images/backCorner.png', alt='')
p
strong=env.t('commGuidePara038')
|&nbsp;
=env.t('commGuidePara038part2')
p=env.t('commGuidePara039')
|&nbsp;
strong=env.t('commGuidePara039part2')
|&nbsp;
=env.t('commGuidePara039part3')
p!=env.t('commGuidePara038')
p!=env.t('commGuidePara039')
h3=env.t('commGuideHeadingTrello')
div(class='clearfix')
img(class='pull-right', src='/community-guidelines-images/trello.png', alt='')
p
strong=env.t('commGuidePara040')
|&nbsp;
=env.t('commGuidePara040part2')
|&nbsp;
strong=env.t('commGuidePara040part3')
|&nbsp;
=env.t('commGuidePara040part4')
p!=env.t('commGuidePara040')
p
strong=env.t('commGuidePara041')
ul
li=env.t('The')
|&nbsp;
strong=env.t('commGuideList03A')
|&nbsp;
=env.t('commGuideList03Apart2')
li=env.t('The')
|&nbsp;
strong=env.t('commGuideList03B')
|&nbsp;
=env.t('commGuideList03Bpart2')
li=env.t('The')
|&nbsp;
strong=env.t('commGuideList03C')
|&nbsp;
=env.t('commGuideList03Cpart2')
li=env.t('The')
|&nbsp;
strong=env.t('commGuideList03D')
|&nbsp;
=env.t('commGuideList03Dpart2')
li=env.t('The')
|&nbsp;
strong=env.t('commGuideList03E')
|&nbsp;
=env.t('commGuideList03Epart2')
p
strong=env.t('commGuidePara042')
|&nbsp;
=env.t('commGuidePara042part2')
li!=env.t('commGuideList03A')
li!=env.t('commGuideList03B')
li!=env.t('commGuideList03C')
li!=env.t('commGuideList03D')
li!=env.t('commGuideList03E')
p!=env.t('commGuidePara042')
h3=env.t('commGuideHeadingGitHub')
div(class='clearfix')
img(class='pull-left', src='/community-guidelines-images/github.gif', alt='')
p
strong=env.t('commGuidePara043')
|&nbsp;
=env.t('commGuidePara043part2')
|&nbsp;
strong=env.t('commGuidePara043part3')
|&nbsp;
=env.t('commGuidePara043part4')
p!=env.t('commGuidePara043')
p
strong=env.t('commGuidePara044')
ul(class='listColumns2 peopleList')
@ -304,10 +186,7 @@ block content
h3=env.t('commGuideHeadingWiki')
div(class='clearfix')
img(class='pull-right', src='/community-guidelines-images/wiki.png', alt='')
p
strong=env.t('commGuidePara045')
|&nbsp;
=env.t('commGuidePara045part2')
p!=env.t('commGuidePara045')
p=env.t('commGuidePara046')
p
strong=env.t('commGuidePara047')
@ -338,10 +217,7 @@ block content
div(class='clearfix')
img(class='pull-left', src='/community-guidelines-images/infractions.png', alt='')
p=env.t('commGuidePara050')
p
strong=env.t('commGuidePara051')
|&nbsp;
=env.t('commGuidePara051part2')
p!=env.t('commGuidePara051')
h4=env.t('commGuideHeadingSevereInfractions')
p=env.t('commGuidePara052')
p=env.t('commGuidePara053')
@ -355,10 +231,7 @@ block content
p=env.t('commGuidePara054')
p=env.t('commGuidePara055')
ul
li=env.t('commGuideList06A')
|&nbsp;(
a(href='mailto:leslie@habitrpg.com') leslie&commat;habitrpg&period;com
|).
li!=env.t('commGuideList06A')
li=env.t('commGuideList06B')
li=env.t('commGuideList06C')
li=env.t('commGuideList06D')
@ -373,10 +246,7 @@ block content
div(class='clearfix')
img(class='pull-right', src='/community-guidelines-images/consequences.png', alt='')
p=env.t('commGuidePara058')
p
strong=env.t('commGuidePara059')
|&nbsp;
=env.t('commGuidePara059part2')
p!=env.t('commGuidePara059')
p
strong=env.t('commGuidePara060')
ul
@ -407,16 +277,9 @@ block content
h3=env.t('commGuideHeadingRestoration')
div(class='clearfix')
img(class='pull-left', src='/community-guidelines-images/restoration.png', alt='')
p=env.t('commGuidePara061')
|&nbsp;
strong=env.t('commGuidePara061part2')
p=env.t('commGuidePara062')
|&nbsp;
strong=env.t('commGuidePara062part2')
p
strong=env.t('commGuidePara063')
|&nbsp;
=env.t('commGuidePara063part2')
p!=env.t('commGuidePara061')
p!=env.t('commGuidePara062')
p!=env.t('commGuidePara063')
h2=env.t('commGuideHeadingContributing')
div(class='clearfix')
@ -433,29 +296,13 @@ block content
p=env.t('commGuidePara065')
p=env.t('commGuidePara066')
ul
li
strong=env.t('commGuideList13A')
|&nbsp;
=env.t('commGuideList13Apart2')
li
strong=env.t('commGuideList13B')
|&nbsp;
=env.t('commGuideList13Bpart2')
li
strong=env.t('commGuideList13C')
|&nbsp;
=env.t('commGuideList13Cpart2')
li
strong=env.t('commGuideList13D')
|&nbsp;
=env.t('commGuideList13Dpart2')
li!=env.t('commGuideList13A')
li!=env.t('commGuideList13B')
li!=env.t('commGuideList13C')
li!=env.t('commGuideList13D')
h2=env.t('commGuideHeadingFinal')
p=env.t('commGuidePara067')
|&nbsp;(
a(href='mailto:leslie@habitrpg.com') leslie&commat;habitrpg&period;com
|)&nbsp;
=env.t('commGuidePara067part2')
p!=env.t('commGuidePara067')
p=env.t('commGuidePara068')
h2=env.t('commGuideHeadingLinks')

View file

@ -8,41 +8,45 @@ block title
title=env.t('titleFront')
block content
.marketing
//we need to use something that's not jumbotron for this, but still keep it centered
//could someone write something else to make it pretty?
img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo')
//this image needs to be replaced by something more enticing, that shows off the features of hRPG
//while acting similarly to a logo
h1#tagline=env.t('tagline')
p.lead
button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton')
hr
img(src='/marketing/devices.png')
//we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron
//it could also be part of the image, as long as the alt text included it
//in fact, I think I really want it on the image, rather than as text, but language issues
br
p.lead=env.t('landingp1')
h2=env.t('landingp2header')
//images in these parts could be useful, too
//if there's a language workaround, image headers? people like pictures!
p.lead
=env.t('landingp2')
|&nbsp;
h2=env.t('landingp3header')
//I'm not sold on "Consquences as the title here. Anyone got a better idea?
p.lead
=env.t('landingp3')
|&nbsp;
h2=env.t('landingp4header')
p.lead=env.t('landingp4')
//- TODO
h2=env.t('landingend')
div(ng-controller='RootCtrl')
include ../shared/header/avatar
include ../shared/modals/members
.marketing
//we need to use something that's not jumbotron for this, but still keep it centered
//could someone write something else to make it pretty?
img(src='/bower_components/habitrpg-shared/img/logo/habitrpg_pixel.png', alt='HabitRPG logo')
//this image needs to be replaced by something more enticing, that shows off the features of hRPG
//while acting similarly to a logo
h1#tagline=env.t('tagline')
p.lead
=env.t('landingend2')
a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink')
=env.t('landingend3')
a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink')
|&nbsp;
=env.t('landingend4')
button.btn.btn-primary.btn-lg#frontpage-play-button(ng-click='playButtonClick()')=env.t('playButton')
hr
img(src='/marketing/devices.png')
//we'd want the tagline centered, for sure, and a bit more pop, but without using jumbotron
//it could also be part of the image, as long as the alt text included it
//in fact, I think I really want it on the image, rather than as text, but language issues
br
p.lead=env.t('landingp1')
h2=env.t('landingp2header')
//images in these parts could be useful, too
//if there's a language workaround, image headers? people like pictures!
p.lead
=env.t('landingp2')
|&nbsp;
h2=env.t('landingp3header')
//I'm not sold on "Consquences as the title here. Anyone got a better idea?
p.lead
=env.t('landingp3')
|&nbsp;
h2=env.t('landingp4header')
p.lead=env.t('landingp4')
//- TODO
h2=env.t('landingend')
p.lead
=env.t('landingend2')
a(href="FEATURESPAGEHERE")=env.t('landingfeatureslink')
=env.t('landingend3')
a(href="ENTERPRISEPAGEHERE")=env.t('landingadminlink')
|&nbsp;
=env.t('landingend4')

View file

@ -3,10 +3,15 @@ script(id='modals/login.html', type='text/ng-template')
button.close(type='button', ng-click='$close()') ×
h4.modal-title=env.t('loginAndReg')
.modal-body(ng-controller='AuthCtrl')
a(href='/auth/facebook')
img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt=env.t('loginFacebookAlt'))
//can we add in google auth? I like google auth
h3=env.t('or')
a.zocial.facebook(alt=env.t('loginFacebookAlt'), ng-click='socialLogin("facebook")')=env.t('loginFacebookAlt')
//-ul.list-inline
li
a.zocial.icon.facebook(alt=env.t('loginFacebookAlt'), ng-click='socialLogin("facebook")')
li
a.zocial.icon.googleplus(alt="Google", ng-click='socialLogin("google")') Google+
li
a.zocial.icon.twitter(alt="Twitter", ng-click='socialLogin("twitter")') Twitter
hr
ul.nav.nav-tabs
li.active
a(data-toggle='tab',data-target='#login-tab')=env.t('login')