mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-05-24 06:35:37 +00:00
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:
parent
16fff701d4
commit
50b1cba0a6
62 changed files with 1607 additions and 1005 deletions
2
.buildpacks
Normal file
2
.buildpacks
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
https://github.com/heroku/heroku-buildpack-nodejs.git
|
||||
https://github.com/stomita/heroku-buildpack-phantomjs.git
|
||||
23
bower.json
23
bower.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
migrations/20141126_turkey_mounts.js
Normal file
11
migrations/20141126_turkey_mounts.js
Normal 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}
|
||||
)
|
||||
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}});
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
// }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
50
src/controllers/members.js
Normal file
50
src/controllers/members.js
Normal 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);
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
77
src/controllers/payments/index.js
Normal file
77
src/controllers/payments/index.js
Normal 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;
|
||||
166
src/controllers/payments/paypal.js
Normal file
166
src/controllers/payments/paypal.js
Normal 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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
68
src/controllers/payments/paypalBillingSetup.js
Normal file
68
src/controllers/payments/paypalBillingSetup.js
Normal 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;
|
||||
}
|
||||
89
src/controllers/payments/stripe.js
Normal file
89
src/controllers/payments/stripe.js
Normal 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;
|
||||
});
|
||||
};
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
));
|
||||
|
||||
|
|
|
|||
58
src/utils.js
58
src/utils.js
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
28
views/avatar-static.jade
Normal 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})
|
||||
|
|
@ -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)};
|
||||
|
||||
|
|
|
|||
|
|
@ -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})')
|
||||
|
|
||||
=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
|
||||
|
|
|
|||
|
|
@ -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()}}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
||||
=env.t('subCanceled')
|
||||
| <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
||||
p.lead
|
||||
=env.t('subscribed')
|
||||
|
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// |
|
||||
// a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
|
||||
// | 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
|
||||
|
|
||||
a(target='_blank', href='/static/community-guidelines')=env.t('communityGuidelines')
|
||||
| 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')
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
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
|
||||
|
|
||||
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}}
|
||||
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}}
|
||||
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
|
||||
|
|
||||
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'))
|
||||
|
|
||||
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}}
|
||||
span(ng-class='::userAdminGlyphiconStyleFromLevel(message.contributor.level)')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
||||
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}}
|
||||
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}}
|
||||
span(ng-class='::userAdminGlyphiconStyle(mod)')
|
||||
p
|
||||
=env.t('communityGuidelinesRead1')
|
||||
|
|
||||
|
|
@ -160,5 +160,4 @@
|
|||
|
|
||||
=env.t('communityGuidelinesRead2')
|
||||
|
||||
ul.list-unstyled.tavern-chat
|
||||
include ./chat-message
|
||||
+chatMessages()
|
||||
|
|
|
|||
|
|
@ -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}}')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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 ☰
|
||||
div(ng-if='_expandedMenu == "social"')
|
||||
ul.toolbar-submenu(ng-click='expandMenu(null)')
|
||||
li
|
||||
a(ui-sref='options.social.inbox')
|
||||
| Inbox
|
||||
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 ☰
|
||||
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')
|
||||
|
|
|
|||
|
|
@ -10,3 +10,4 @@ include ./classes
|
|||
include ./quests
|
||||
include ./rebirth
|
||||
include ./limited
|
||||
include ./invite-friends
|
||||
|
|
|
|||
48
views/shared/modals/invite-friends.jade
Normal file
48
views/shared/modals/invite-friends.jade
Normal 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')
|
||||
|
|
||||
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
|
||||
|
||||
|
|
@ -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')
|
||||
|
|
||||
=env.t('memberSince')
|
||||
|
|
||||
| {{timestamp(profile.auth.timestamps.created)}} -
|
||||
li(ng-show='profile.auth.timestamps.loggedin')
|
||||
| {{::timestamp(profile.auth.timestamps.created)}} -
|
||||
li(ng-show='::profile.auth.timestamps.loggedin')
|
||||
|
|
||||
=env.t('lastLoggedIn')
|
||||
|
|
||||
| {{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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
||||
=env.t('commGuideList01Apart2')
|
||||
li
|
||||
strong=env.t('commGuideList01B')
|
||||
|
|
||||
=env.t('commGuideList01Bpart2')
|
||||
li
|
||||
strong=env.t('commGuideList01C')
|
||||
|
|
||||
=env.t('commGuideList01Cpart2')
|
||||
li
|
||||
strong=env.t('commGuideList01D')
|
||||
|
|
||||
=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')
|
||||
| (
|
||||
a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com
|
||||
|).
|
||||
|
||||
p!=env.t('commGuidePara012')
|
||||
p=env.t('commGuidePara013')
|
||||
p=env.t('commGuidePara014')
|
||||
|
|
||||
|
|
@ -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')
|
||||
|
|
||||
=env.t('commGuidePara017part2')
|
||||
p!=env.t('commGuidePara017')
|
||||
ul
|
||||
li
|
||||
strong=env.t('commGuideList02A')
|
||||
li
|
||||
strong=env.t('commGuideList02B')
|
||||
|
|
||||
=env.t('commGuideList02Bpart2')
|
||||
li
|
||||
strong=env.t('commGuideList02C')
|
||||
|
|
||||
=env.t('commGuideList02Cpart2')
|
||||
li
|
||||
strong=env.t('commGuideList02D')
|
||||
|
|
||||
=env.t('commGuideList02Dpart2')
|
||||
li
|
||||
strong=env.t('commGuideList02E')
|
||||
|
|
||||
=env.t('commGuideList02Epart2')
|
||||
li
|
||||
strong=env.t('commGuideList02F')
|
||||
|
|
||||
=env.t('commGuideList02Fpart2')
|
||||
li
|
||||
strong=env.t('commGuideList02G')
|
||||
|
|
||||
=env.t('commGuideList02Gpart2')
|
||||
li
|
||||
strong=env.t('commGuideList02H')
|
||||
|
|
||||
=env.t('commGuideList02Hpart2')
|
||||
|
|
||||
a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com
|
||||
|
|
||||
=env.t('commGuideList02Hpart3')
|
||||
p
|
||||
strong=env.t('commGuidePara019')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
strong=env.t('commGuidePara024part2')
|
||||
|
|
||||
=env.t('commGuidePara024part3')
|
||||
p
|
||||
strong=env.t('commGuidePara027')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=env.t('commGuidePara029part2')
|
||||
|
|
||||
strong=env.t('commGuidePara029part3')
|
||||
p
|
||||
strong=env.t('commGuidePara031')
|
||||
|
|
||||
=env.t('commGuidePara031part2')
|
||||
p
|
||||
strong=env.t('commGuidePara033')
|
||||
|
|
||||
=env.t('commGuidePara033part2')
|
||||
|
|
||||
a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com
|
||||
|
|
||||
=env.t('commGuidePara033part3')
|
||||
p
|
||||
strong=env.t('commGuidePara035')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=env.t('commGuidePara038part2')
|
||||
p=env.t('commGuidePara039')
|
||||
|
|
||||
strong=env.t('commGuidePara039part2')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=env.t('commGuidePara040part2')
|
||||
|
|
||||
strong=env.t('commGuidePara040part3')
|
||||
|
|
||||
=env.t('commGuidePara040part4')
|
||||
p!=env.t('commGuidePara040')
|
||||
p
|
||||
strong=env.t('commGuidePara041')
|
||||
ul
|
||||
li=env.t('The')
|
||||
|
|
||||
strong=env.t('commGuideList03A')
|
||||
|
|
||||
=env.t('commGuideList03Apart2')
|
||||
li=env.t('The')
|
||||
|
|
||||
strong=env.t('commGuideList03B')
|
||||
|
|
||||
=env.t('commGuideList03Bpart2')
|
||||
li=env.t('The')
|
||||
|
|
||||
strong=env.t('commGuideList03C')
|
||||
|
|
||||
=env.t('commGuideList03Cpart2')
|
||||
li=env.t('The')
|
||||
|
|
||||
strong=env.t('commGuideList03D')
|
||||
|
|
||||
=env.t('commGuideList03Dpart2')
|
||||
li=env.t('The')
|
||||
|
|
||||
strong=env.t('commGuideList03E')
|
||||
|
|
||||
=env.t('commGuideList03Epart2')
|
||||
p
|
||||
strong=env.t('commGuidePara042')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=env.t('commGuidePara043part2')
|
||||
|
|
||||
strong=env.t('commGuidePara043part3')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=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')
|
||||
| (
|
||||
a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.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')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
strong=env.t('commGuidePara061part2')
|
||||
p=env.t('commGuidePara062')
|
||||
|
|
||||
strong=env.t('commGuidePara062part2')
|
||||
p
|
||||
strong=env.t('commGuidePara063')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
=env.t('commGuideList13Apart2')
|
||||
li
|
||||
strong=env.t('commGuideList13B')
|
||||
|
|
||||
=env.t('commGuideList13Bpart2')
|
||||
li
|
||||
strong=env.t('commGuideList13C')
|
||||
|
|
||||
=env.t('commGuideList13Cpart2')
|
||||
li
|
||||
strong=env.t('commGuideList13D')
|
||||
|
|
||||
=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')
|
||||
| (
|
||||
a(href='mailto:leslie@habitrpg.com') leslie@habitrpg.com
|
||||
|)
|
||||
=env.t('commGuidePara067part2')
|
||||
p!=env.t('commGuidePara067')
|
||||
p=env.t('commGuidePara068')
|
||||
|
||||
h2=env.t('commGuideHeadingLinks')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
||||
h2=env.t('landingp3header')
|
||||
//I'm not sold on "Consquences as the title here. Anyone got a better idea?
|
||||
p.lead
|
||||
=env.t('landingp3')
|
||||
|
|
||||
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')
|
||||
|
|
||||
=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')
|
||||
|
|
||||
h2=env.t('landingp3header')
|
||||
//I'm not sold on "Consquences as the title here. Anyone got a better idea?
|
||||
p.lead
|
||||
=env.t('landingp3')
|
||||
|
|
||||
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')
|
||||
|
|
||||
=env.t('landingend4')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in a new issue