diff --git a/.bowerrc b/.bowerrc
index 20772d5d24..9c9ea40600 100644
--- a/.bowerrc
+++ b/.bowerrc
@@ -1,8 +1,3 @@
{
- "directory": "public/bower_components",
- "storage": {
- "packages": ".bower-cache",
- "registry": ".bower-registry"
- },
- "tmp": ".bower-tmp"
+ "directory": "public/bower_components"
}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index bb5ec66ebf..4a6eb3925d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ newrelic_agent.log
.bower-tmp
.bower-registry
.bower-cache
+
+*.log
src/*/*.map
src/*/*/*.map
test/*.js
diff --git a/.nodemonignore b/.nodemonignore
index f4e0b53926..5cfae9cb6a 100644
--- a/.nodemonignore
+++ b/.nodemonignore
@@ -6,4 +6,6 @@ Gruntfile.js
CHANGELOG.md
.idea*
.git*
+*.log
newrelic_agent.log
+.bower-cache*
diff --git a/Gruntfile.js b/Gruntfile.js
index e5fa1ed3ed..d778230751 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -4,6 +4,8 @@ module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
+ pkg: grunt.file.readJSON('package.json'),
+
git_changelog: {
minimal: {
options: {
@@ -56,7 +58,10 @@ module.exports = function(grunt) {
build: {
files: [
{expand: true, cwd: 'public/', src: 'favicon.ico', dest: 'build/'},
- {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/dist/spritesmith-*.png', dest: 'build/'}
+ {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/dist/spritesmith.png', dest: 'build/'},
+ {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/img/sprites/backer-only/*.gif', dest: 'build/'},
+ {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/img/sprites/npc_ian.gif', dest: 'build/'},
+ {expand: true, cwd: 'public/', src: 'bower_components/bootstrap/dist/fonts/*', dest: 'build/'}
]
}
},
@@ -69,16 +74,18 @@ module.exports = function(grunt) {
},
src: [
'build/*.js', 'build/*.css', 'build/favicon.ico',
- 'build/bower_components/bootstrap/docs/assets/css/*.css',
- 'build/bower_components/habitrpg-shared/dist/*.css'
+ 'build/bower_components/habitrpg-shared/dist/*.png',
+ 'build/bower_components/habitrpg-shared/img/sprites/backer-only/*.gif',
+ 'build/bower_components/habitrpg-shared/img/sprites/npc_ian.gif',
+ 'build/bower_components/bootstrap/dist/fonts/*'
],
- dest: 'make-sure-i-do-not-exist'
+ dest: 'build/*.css'
}
},
- nodemon: {
+ nodemon: {
dev: {
- ignoredFiles: ['public/*', 'Gruntfile.js', 'CHANGELOG.md', 'views/*', 'build/*', '.idea*', '.git*']
+ script: '<%= pkg.main %>'
}
},
@@ -106,27 +113,35 @@ module.exports = function(grunt) {
var files = grunt.file.readJSON('./public/manifest.json');
var uglify = {};
var cssmin = {};
+
_.each(files, function(val, key){
+
var js = uglify['build/' + key + '.js'] = [];
+
_.each(files[key]['js'], function(val){
js.push('public/' + val);
});
+
+ var css = cssmin['build/' + key + '.css'] = [];
+
_.each(files[key]['css'], function(val){
- if(val == 'app.css' || val == 'static.css'){
- cssmin['build/' + val] = ['build/' + val]
- }else{
- cssmin['build/' + val] = ['public/' + val]
- }
+ var path = (val == 'app.css' || val == 'static.css') ? 'build/' : 'public/';
+ css.push(path + val)
});
+
});
+
grunt.config.set('uglify.build.files', uglify);
- grunt.config.set('uglify.build.options', {compress: false})
+ grunt.config.set('uglify.build.options', {compress: false});
+
grunt.config.set('cssmin.build.files', cssmin);
+ // Rewrite urls to relative path
+ grunt.config.set('cssmin.build.options', {'target': 'public/css/whatever-css.css'});
});
// Register tasks.
grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']);
- grunt.registerTask('build:dev', ['loadManifestFiles', 'clean:build', 'stylus', 'cssmin', 'copy:build']);
+ grunt.registerTask('build:dev', ['stylus']);
grunt.registerTask('run:dev', [ 'build:dev', 'concurrent' ]);
diff --git a/bower.json b/bower.json
index 74fea1a96d..a6de245fda 100644
--- a/bower.json
+++ b/bower.json
@@ -14,20 +14,21 @@
"tests"
],
"dependencies": {
- "jquery": "~2.0.3",
+ "jquery": "~2.1.0",
"jquery.cookie": "~1.4.0",
"jquery-ui": "~1.10.3",
- "angular": "~1.2.1",
- "angular-sanitize": "~1.2.1",
- "angular-resource": "~1.2.1",
- "angular-ui": "~0.4.0",
- "angular-bootstrap": "~0.5.0",
+ "angular": "~1.2.12",
+ "angular-sanitize": "~1.2.12",
+ "angular-resource": "~1.2.12",
+ "angular-ui-utils": "~0.1.0",
+ "angular-ui-select2": "https://github.com/angular-ui/ui-select2.git#4469fd7a32eba2e28d5e3dd319b49e1ad090b167",
+ "angular-bootstrap": "~0.10.0",
"angular-ui-router": "ca3b4777a603df8f86cfd653c8f6c38b2ae05d89",
- "angular-loading-bar": "~0.0.5",
+ "angular-loading-bar": "~0.3.0",
"angular-bindonce": "~0.2.1",
- "bootstrap": "v2.3.2",
- "bootstrap-datepicker": "~1.2.0",
- "bootstrap-growl": "~1.1.0",
+ "bootstrap": "~3.1.0",
+ "bootstrap-growl": "git://github.com/ifightcrime/bootstrap-growl.git#master",
+ "bootstrap-tour": "~0.8.1",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#develop",
"BrowserQuest": "https://github.com/mozilla/BrowserQuest.git",
"github-buttons": "git://github.com/mdo/github-buttons.git",
@@ -35,15 +36,12 @@
"JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master",
"Angular-At-Directive": "git://github.com/snicker/Angular-At-Directive#master",
"js-emoji": "git://github.com/snicker/js-emoji#master",
- "gemoji": "git://github.com/github/gemoji",
"sticky": "*",
- "bootstrap-tour": "~0.8.0",
- "angular-ui-utils": "~0.1.0",
"swagger-ui": "https://github.com/wordnik/swagger-ui.git#105c516f5f055b140e935e9cfe0c36f841921dff",
"ngInfiniteScroll": "~1.0.0",
- "angular-ui-select2": "https://github.com/angular-ui/ui-select2.git#4469fd7a32eba2e28d5e3dd319b49e1ad090b167"
+ "jquery-colorbox": "~1.4.36"
},
"devDependencies": {
- "angular-mocks": "~1.2.1"
+ "angular-mocks": "~1.2.12"
}
}
diff --git a/config.json.example b/config.json.example
index 634d22c7a7..da9b3bb238 100644
--- a/config.json.example
+++ b/config.json.example
@@ -11,8 +11,12 @@
"SMTP_USER":"user@domain.com",
"SMTP_PASS":"password",
"SMTP_SERVICE":"Gmail",
+ "SMTP_HOST":"smtp.gmail.com",
+ "SMTP_PORT": 587,
+ "SMTP_TLS": true,
"STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY":"22223333444455556666777788889999",
"PAYPAL_MERCHANT":"paypal-merchant@gmail.com",
- "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY"
+ "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY",
+ "GA_ID": "GA_ID"
}
diff --git a/migrations/contribs_plan.js b/migrations/contribs_plan.js
new file mode 100644
index 0000000000..a08bd93f76
--- /dev/null
+++ b/migrations/contribs_plan.js
@@ -0,0 +1,23 @@
+// Give contrib.level 7+ free subscription for life
+db.users.update(
+
+ {
+ 'contributor.level':{$gte:7},
+ 'purchased.plan.customerId':null
+ },
+
+ {
+ $set: {
+ 'purchased.plan':{
+ planId: 'basic',
+ customerId: 'habitrpg',
+ dateCreated: new Date,
+ dateUpdated: new Date,
+ gemsBought: 0
+ }
+ }
+ },
+
+ {multi:true}
+
+)
\ No newline at end of file
diff --git a/migrations/missing_gems.js b/migrations/missing_gems.js
index ca9f674b9a..78ff37ed1d 100644
--- a/migrations/missing_gems.js
+++ b/migrations/missing_gems.js
@@ -1 +1 @@
-db.users.update({_id:''},{$set:{'purchased.ads':true},$inc:{balance:5}});
\ No newline at end of file
+db.users.update({_id:''},{$inc:{balance:5}});
\ No newline at end of file
diff --git a/package.json b/package.json
index 338485de34..b3a788169d 100644
--- a/package.json
+++ b/package.json
@@ -40,12 +40,16 @@
"pretty-data": "git://github.com/vkiryukhin/pretty-data#master",
"js2xmlparser": "~0.1.2",
"mongoose": "~3.8.1",
- "domain-middleware": "~0.1.0",
"swagger-node-express": "git://github.com/lefnire/swagger-node-express#habitrpg",
"passport": "~0.1.18",
"passport-facebook": "~1.0.2",
"newrelic": "~1.3.0",
- "connect-ratelimit": "0.0.6"
+ "connect-ratelimit": "0.0.6",
+ "winston": "~0.7.2",
+ "winston-mail": "~0.2.7",
+ "winston-newrelic": "~0.1.4",
+ "domain-middleware": "~0.1.0",
+ "universal-analytics": "~0.3.2"
},
"private": true,
"subdomain": "habitrpg",
diff --git a/public/css/alerts.styl b/public/css/alerts.styl
index 4b765329b1..40a7b8910b 100644
--- a/public/css/alerts.styl
+++ b/public/css/alerts.styl
@@ -16,6 +16,27 @@
.wide-popover
max-width: 400px
+// Small alerts
+alert-sm-mixin()
+ padding-top: 8px
+ padding-bottom: 8px
+
+.alert-sm
+ alert-sm-mixin()
+
+#notification-area
+ position: fixed
+ top: 0
+ right: 0px
+ z-index: 1061
+
+ .alert
+ alert-sm-mixin()
+
+ .notification-icon
+ transform: scale(0.8)
+
+
// variables
gpColor = #FBEFC0
@@ -78,30 +99,6 @@ borderDarken = 20%
border-color: darken(dropColor,borderDarken)
color: dropText
-// alert icons
-
-.icon-gold
- background: url("/bower_components/habitrpg-shared/img/coin_single_gold.png") no-repeat
- background-position: center center
- background-size: 18px
- width: 14px
- height: 14px
-
-.icon-silver
- background: url("/bower_components/habitrpg-shared/img/coin_single_silver.png") no-repeat
- background-position: center center
- background-size: 18px
- width: 14px
- height: 14px
-
-.icon-death
- background: url("/bower_components/habitrpg-shared/img/sprites/dead.png") no-repeat
- background-position: center center
- background-size: 14px
- width: 14px
- height: 14px
-
-
.undo-button
z-index: 3000
position:absolute
diff --git a/public/css/avatar.styl b/public/css/avatar.styl
index 3a774d4f65..5afe496a29 100644
--- a/public/css/avatar.styl
+++ b/public/css/avatar.styl
@@ -9,6 +9,16 @@ 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
+
+.avatar-level
+ position absolute
+ bottom 0px
+ right 0px
+ padding 2px 3px 2px 3px
+
// basic herobox styles
.herobox
height: 10.5em // higher to acct for the name area
@@ -40,7 +50,7 @@ future re: pets and whatnot, this is just temporary.
// always visible on the user's own avatar
.herobox:after
opacity: 0
-.herobox.isUser:after
+.herobox.isUser:not(.hasMount):after
opacity: 1
// make it a bit special
background darken($better, 15%)
@@ -52,21 +62,17 @@ future re: pets and whatnot, this is just temporary.
.active
border 1px solid black
-// vertically center avatar sprite
-// depending on if they have a pet
-.herobox.hasPet
- padding-top: 2em
-.herobox:not(.hasPet)
- padding-top: 1.75em
-
-// if it is the user's avatar, check if
-// they have a pet and pad accordingly
-.herobox.hasPet.isUser
- padding-top: 3.25em
-
// if not user's avatar, remove lines for party unity
.herobox:not(.isUser)
- outline: 0
+ outline: 0
+
+// vertically center avatar sprite depending on if they have a pet
+.herobox.hasPet
+ padding-top: 1.75em
+.herobox:not(.hasPet)
+ padding-top: 2em
+.herobox.hasMount
+ padding-top:0em
// expose hero info on hover, taking
// into account the extra pet space
@@ -74,13 +80,6 @@ future re: pets and whatnot, this is just temporary.
background: desaturate(lighten($better, 30%), 10%)
&:after
opacity: 1
-/* .herobox.hasPet
- &:hover, &:focus
- padding-top: 3.25em
-.herobox:not(.hasPet)
- &:hover, &:focus
- padding-top: 2.5em */
-
// positioning the sprites, etc
.herobox
@@ -93,4 +92,4 @@ future re: pets and whatnot, this is just temporary.
position: absolute
.herobox.isLeader
- background: $better
+ background: $better
\ No newline at end of file
diff --git a/public/css/challenges.styl b/public/css/challenges.styl
index 98d12edb17..d3a39eaca1 100644
--- a/public/css/challenges.styl
+++ b/public/css/challenges.styl
@@ -10,12 +10,4 @@ ul.challenge-accordion-header-specs
margin-bottom: 10px
#challenges-filters h3
- margin-top: 0px;
-
-// for things like challenges, member profiles, etc
-@media (min-width: 767px)
- .wide-modal
- /* new custom width */
- width: 1020px;
- /* must be half of the width, minus scrollbar on the left (30px) */
- margin-left: -480px;
\ No newline at end of file
+ margin-top: 0px;
\ No newline at end of file
diff --git a/public/css/customizer.styl b/public/css/customizer.styl
index fd3ad1add1..f4317afe2e 100644
--- a/public/css/customizer.styl
+++ b/public/css/customizer.styl
@@ -22,15 +22,18 @@ menu
line-height: 2
.customize-option
- border: 0px solid grey;
- background-color: hsl(0, 0%, 93%);
+ border: 0px solid grey
+ background-color: hsl(0, 0%, 93%)
margin-bottom: 10px
- cursor: pointer;
-webkit-transition: background-color 0.5s ease-out
-moz-transition: background-color 0.5s ease-out
transition: background-color 0.5s ease-out
+ &.hair
+ width: 40px !important
+ height: 40px !important
+
.customize-option:active
background-color: rgb(255, 242, 204);
-webkit-transition: none
diff --git a/public/css/filters.styl b/public/css/filters.styl
index 1c68eb6470..6512c3e0b3 100644
--- a/public/css/filters.styl
+++ b/public/css/filters.styl
@@ -1,36 +1,25 @@
-/* Filters */
.filters
- padding-left: 19px
- padding-right: 19px
-.filters .nav
- margin-bottom: 0
-
-.filter-description
- margin-top: 7px
- margin-right: 7px
-
-/* Filters applied to tasks */
-.task.filtered-out
- display: none
-
-.tag-label:hover
- background-color: #3a87ad
- cursor: pointer
-
- .tag-editing
- padding 2px 0 0 2px
-
- .tag-editing .tag-editing-pill
- height:auto
+ margin-top: 5px
+ margin-bottom: 5px
-.tag-list
- overflow-y: auto
- white-space: nowrap
-
- .nav-pills
- height: 43px
-
- li
- display: inline-block
- float: none
\ No newline at end of file
+ .filter-description
+ margin-top: 7px
+ margin-right: 7px
+ padding-left: 7px
+
+ markdown
+ display: inline-block
+
+ .glyphicon-bullhorn
+ margin-right: 5px
+
+ .input-group
+ float: left
+ width: 1px // Trick for proper alignment
+ margin-left: 3px
+ margin-right: 3px
+ margin-top: 5px
+
+ .form-control
+ width: auto
\ No newline at end of file
diff --git a/public/css/footer.styl b/public/css/footer.styl
index 0f39a09f1b..ad03be3f23 100644
--- a/public/css/footer.styl
+++ b/public/css/footer.styl
@@ -1,83 +1,9 @@
-/* Sticky footer styles
--------------------------------------------------- */
-
-html,
-body {
- height: 100%;
- /* The html and body elements cannot have any padding or margin. */
-}
-
-/* Wrapper for page content to push down footer */
-#wrap {
- min-height: 100%;
- height: auto !important;
- height: 100%;
- /* Negative indent footer by it's height */
- margin: 0 auto -60px;
-}
-
-/* Set the fixed height of the footer here */
-#push,
-#footer {
- height: 60px;
-}
-#footer {
- background-color: #f5f5f5;
-}
-
-/* Lastly, apply responsive CSS fixes as necessary */
-@media (max-width: 767px) {
- #footer {
- margin-left: -20px;
- margin-right: -20px;
- padding-left: 20px;
- padding-right: 20px;
- }
-}
-
-
-
-/* Custom page CSS
--------------------------------------------------- */
-/* Not required for template or sticky footer method. */
-
-#wrap > .container {
- padding-top: 60px;
-}
-.container .credit {
- margin: 20px 0;
-}
-
-code {
- font-size: 80%;
-}
-
-
-
/* HabitRPG Custom CSS
-------------------------------------------------- */
-.footer {
- /*padding: 70px 0;*/
- border-top: 1px solid #e5e5e5;
- background-color: #f5f5f5;
-}
-
-.footer li {
- text-align: left;
-}
-
-.footer h4 {
- text-align: left;
-}
-
-.footer td {
- align: left !important;
-}
-
-.frontpage-play-button
- //box-shadow: 0 0 40px #494141;
- display:inline
- font-size: 20px
- width:100px
- height:50px
\ No newline at end of file
+.footer
+ padding-top: 20px
+ margin-top: 40px
+ padding-bottom: 20px
+ border-top: 1px solid #e5e5e5
+ background-color: #f5f5f5
\ No newline at end of file
diff --git a/public/css/game-pane.styl b/public/css/game-pane.styl
index 346e86f44c..a3aaf0151e 100644
--- a/public/css/game-pane.styl
+++ b/public/css/game-pane.styl
@@ -1,6 +1,3 @@
-.full-width
- width 100%
-
.border-right
border-right 1px solid #ddd
@@ -8,21 +5,27 @@
color #fff
background-color #51a351
-.tavern-pane
- position relative
- height 250px
-
.chat-form
- position: relative
- z-index: 10
+ margin-bottom: 10px
-.chat-message .chat-plus-one
- opacity: 0
-.chat-message:hover .chat-plus-one
- opacity: 1
+ textarea
+ margin-bottom: 5px
+
+.chat-message
+ markdown, span.time
+ padding: 0 3px
+
+ .chat-plus-one
+ opacity: 0
+ background-color: #eee
+ padding: 3px 3px 0px 3px
+
+ &:hover .chat-plus-one
+ opacity: 1
+ color: #999
.tavern-chat, .party-chat
-
+
li
padding-top:15px
padding-bottom:15px
@@ -78,6 +81,11 @@
padding-top:0px
padding-bottom:0px
+.panel-tiers
+ div
+ display: none
+ margin-top: 10px
+
// Name tags
.label-contributor-1, .label-contributor-2
background-color: #333;
@@ -108,33 +116,33 @@
padding: 0px 5px
// Autocomplete [TODO] make this nicer
-.list-at-user {
- width: 100%;
- max-width: 120px;
- position: absolute;
- background: white;
- border: 1px solid #a4a4a4;
- z-index: 10;
-}
+.list-at-user
+ width: 100%
+ max-width: 120px
+ position: absolute
+ background: white
+ border: 1px solid #a4a4a4
+ z-index: 10
-.list-at-user li {
- line-height:14px;
- border-bottom: 1px solid #a4a4a4;
- list-style-type: none;
-}
-.list-at-user li:hover {
- cursor: pointer;
- background: #b9dff4;
-}
+.list-at-user li
+ line-height:14px
+ border-bottom: 1px solid #a4a4a4
+ list-style-type: none
-.list-at-user li span {
- margin: 2px;
- font-size: 11.844px;
- margin-left: 5px;
- display: inline-block;
-}
-.list-cur {
- background: #b9dff4;
-}
+.list-at-user li:hover
+ cursor: pointer
+ background: #b9dff4
+
+
+.list-at-user li span
+ margin: 2px
+ font-size: 11.844px
+ margin-left: 5px
+ display: inline-block
+
+
+.list-cur
+ background: #b9dff4
+
diff --git a/public/css/index.styl b/public/css/index.styl
index fb2d74a615..7ecb15ca2b 100644
--- a/public/css/index.styl
+++ b/public/css/index.styl
@@ -3,9 +3,6 @@
// Vendor Includes - include first so we can override
// Import only styles that do not have urls to images! Include them directly in the page!
-@import "../bower_components/bootstrap-datepicker/css/datepicker.css"
-@import "../bower_components/angular-ui/build/angular-ui.min.css"
-@import "../bower_components/bootstrap/docs/assets/css/bootstrap-responsive.css"
@import "../bower_components/bootstrap-tour/build/css/bootstrap-tour.min.css"
@import "../bower_components/angular-loading-bar/build/loading-bar.css"
@import "../bower_components/js-emoji/emoji.css"
@@ -19,7 +16,6 @@
@import "./inventory.styl"
@import "./alerts.styl"
@import "./helpers.styl"
-@import "./responsive.styl"
@import "./header.styl"
@import "./filters.styl"
@import "./scrollbars.styl"
@@ -29,10 +25,7 @@
@import "./classes.styl"
@import "./quests.styl"
@import "./shared.styl"
-
-// fix exploding to very wide for some reason
-.datepicker
- width: 210px
+@import "./footer.styl"
html,body,p,h1,ul,li,table,tr,th,td
margin: 0
@@ -42,54 +35,27 @@ html,body,p,h1,ul,li,table,tr,th,td
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
box-sizing: border-box
-
-* { font-family: "Lato", sans-serif }
+ font-family: "Lato", sans-serif
hr
border-top: 0
border-bottom: 1px solid #ddd
border-color: rgba(0,0,0,0.1)
-
-/* Footer
--------------------------------------------------- */
-
-/* Bootstrap website boilerplate */
-.footer {
- padding: 70px 0;
- border-top: 1px solid #e5e5e5;
- background-color: #f5f5f5;
-}
-.footer p {
- margin-bottom: 0;
- color: #777;
-}
-.footer-links {
- margin: 10px 0;
-}
-.footer-links li {
- display: inline;
- margin-right: 10px;
-}
-
-/* Bootstrap overrides */
-div.tooltip-inner {
- max-width: 300px;
-}
-
/* Customizations to make footer sticky */
-/*html, body {height: 100%;}*/
-#wrap
- min-height: 100%
- z-index:-1
-#main
- padding-bottom: 250px; /* don't know why this works, sticky footers are weird */
-@media (max-width: 600px) {
- #wrap {
+html, body
+ height: 100%
+
+#wrap
+ z-index:-1
+ padding: 0 15px // Required by Bootstrap 3 when using .container-fluid
+ min-height: 100%
+ height: auto !important
+
+@media (max-width: 600px)
+ #wrap
margin-top: 0
- }
-}
/* Gems
-------------------------------------------------- */
@@ -108,17 +74,17 @@ div.tooltip-inner {
position: relative;
.gem-cost::before
- content: "";
- display: block;
- width: 0;
- height: 0;
- border: 6px solid #D4D4D4;
- border-color: transparent #D4D4D4 transparent transparent;
- position: absolute;
- right: 100%;
- margin-right: 0;
- top: 50%;
- margin-top: -6px;
+ content: ""
+ display: block
+ width: 0
+ height: 0
+ border: 6px solid #D4D4D4
+ border-color: transparent #D4D4D4 transparent transparent
+ position: absolute
+ right: 100%
+ margin-right: 0
+ top: 50%
+ margin-top: -6px
.gem-cost::after
content: "";
@@ -135,68 +101,81 @@ div.tooltip-inner {
/* Misc
-------------------------------------------------- */
+//move
-.modal
- whitespace: normal
+.new-stuff-modal // a .modal-body
+ h5
+ font-weight: 700
-.notification-character
- float:left
+.modal-fixed-height
+ overflow-y: auto
+ max-height: 400px // Taken from Bootstrap 2.3.2 css, may need to change
-.notification-message
- display: inline-block;
- padding-right: 1em
- line-height: 3
-
-.notification-action
- vertical-align: baseline
-#reset-modal
- z-index:1500
-#notification-area
- position: fixed;
- top: 0;
- right: 0px;
- z-index: 1061;
+// notification-character used only by death modal's figure, move
.modal-body
- margin: 10px
+ figure
+ float: left
+ padding: 15px
-
-// TODO this is supposed to go in avatar.styl, but stylus is breaking if I even touch that file
-.profile-modal-header
- width:560px
- margin: 0px auto
-
-.avatar-level
- position absolute
- bottom 0px
- right 0px
- padding 1px 3px 1px 3px
+.death-modal // a .modal-body
+ margin: 10px
a
cursor: pointer
-#modalMember p
- word-wrap: break-word
+a span.glyphicon
+ color: #333 // Takes base color from bootstrap
+
+a.label
+ color: #fff
+
+.label .glyphicon
+ color: #fff
+
+.muted
+ color: #999
.btn
margin-right: 5px
+.vertical-align
+ vertical-align: middle
+.inline-block
+ display: inline-block
+
+// Buttons in groups should not have a 5px margin between them; suppress margin for buttons in groups.
+.btn-group .btn
+ margin-right: 0px
+
.modal-indented-list
margin-left: 10px;
padding-left: 10px;
-.inline-modal
- position: relative
- top: auto;
- left: auto
- right: auto
- margin: 0 auto 20px
- z-index: 1
- max-width: 100%
-
.buy-gems
.nav-tabs
margin-top: 0.5em
.gem-wallet
padding-top: 2px
+// Add badges' classes
+.badge-info
+ background-color: #428bca
+.badge-success
+ background-color: #47a447
+
+.white, .white a
+ color: #fff !important
+
+.collapse-toolbar
+ position:absolute
+ right:0px
+ top:0px
+ width:30px
+ height:25px
+ background-color:#222
+ z-index:999
+ .glyphicon
+ color:white
+ padding-left:7px
+ padding-top:5px
diff --git a/public/css/inventory.styl b/public/css/inventory.styl
index 0dbf947dec..44c4819343 100644
--- a/public/css/inventory.styl
+++ b/public/css/inventory.styl
@@ -15,29 +15,12 @@
.modal-header .gem-wallet
padding-top: 0px
+.equipment-title
+ display: inline-block
+ vertical-align: middle
-// pets (this will all change when pet system is overhauled)
-.pet-grid
- padding-top: 1.5em
- border-top: rgba(0,0,0,0.1)
- table
- width: 100%
- td
- padding: 0.5em
- //width: 25%
- &.active-pet
- background-color: $bad
- outline: 1px solid rgba(0,0,0,0.1)
- outline-offset: -1px
- &:hover, &:focus
- background-color: darken($better, 10%)
- > div
- margin:auto
- margin-bottom:.5em
- p
- text-align:center
- width:6.5em
- height:2.5em
+.stable .static-popover
+ max-width: 550px
menu.pets div
display: inline-block
@@ -57,12 +40,13 @@ menu.pets div
// display:block
.food-tray
- position:fixed
- right:0px
- bottom:0px
- width:30%
+ position: fixed
+ right: 15px
+ bottom: 0px
+ width: 30%
height: 50%
- overflow-y: scroll
+ overflow-y: auto
+ z-index: 1
.inventory-list
p
diff --git a/public/css/npcs.styl b/public/css/npcs.styl
index 48493536b3..4f55489d5c 100644
--- a/public/css/npcs.styl
+++ b/public/css/npcs.styl
@@ -4,15 +4,15 @@
.npc_matt
margin-bottom: 20px
+.toolbar-bailey-container
+ width:55px
+ position:relative
.npc_bailey
float:left
.npc_bailey_head
- //height: 30px !important
- height: 50px !important
position: absolute
- //top: 117px // 147 (header-height) - 30 (bailey height)
- top: 97px
- right: 50px
+ top: 4px
+ height:45px !important
cursor: pointer
// Tour (Justin)
diff --git a/public/css/responsive.styl b/public/css/responsive.styl
deleted file mode 100644
index c7200751f8..0000000000
--- a/public/css/responsive.styl
+++ /dev/null
@@ -1,61 +0,0 @@
-// widths
-.full
- width: 100%;
-.half
- width: 50%;
-.third
- width: 33.3333%;
-.two-thirds
- width: 66.6666%;
-.quarter
- width: 25%;
-.three-quarters
- width: 75%;
-.fifth
- width: 20%;
-.two-fifths
- width: 40%;
-.three-fifths
- width: 60%;
-.four-fifths
- width: 80%;
-.sixth
- width: 16.6667%;
-.eighth
- width: 12.5%;
-.tenth
- width: 10%;
-.twelfth
- width: 8.3333%;
-
-.grid
- padding: 0 1.5em 1.5em 0
- letter-spacing: -4px
-
-.module
- display: inline-block
- letter-spacing: normal
- vertical-align: top
- width: 25%
- padding: 0 0 1.5em 1.5em
- -moz-box-sizing: border-box
- box-sizing: border-box
-
-// at 960px when 4 columns really starts to break
-@media (max-width: 60em)
- .module
- width: 50%
- .task-column
- max-height: 18em
- overflow-y: scroll
-
-// at 600px when 2col starts breaking
-@media (max-width: 37.5em)
- .grid
- padding-right: 0
- .module
- width: 100%
- padding-left: 0
- .task-column
- max-height: none
- overflow: visible
\ No newline at end of file
diff --git a/public/css/shared.styl b/public/css/shared.styl
index 1766422a59..1d4b71b780 100644
--- a/public/css/shared.styl
+++ b/public/css/shared.styl
@@ -13,4 +13,14 @@
.dashed-underline
border-bottom:1px dashed #000
text-decoration:none
- cursor pointer
\ No newline at end of file
+ cursor pointer
+
+// Required by angular-ui-bootstrap
+.nav, .pagination, .carousel, .panel-title a
+ cursor: pointer
+
+.nav-tabs
+ margin-bottom: 15px
+
+img
+ max-width: 100%
\ No newline at end of file
diff --git a/public/css/static-pages.css b/public/css/static-pages.css
deleted file mode 100644
index f617e486e2..0000000000
--- a/public/css/static-pages.css
+++ /dev/null
@@ -1,21 +0,0 @@
-.jumbotron {
- background-color: #1b1b1b;
- background-image: -moz-linear-gradient(top, #222222, #111111);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));
- background-image: -webkit-linear-gradient(top, #222222, #111111);
- background-image: -o-linear-gradient(top, #222222, #111111);
- background-image: linear-gradient(to bottom, #222222, #111111);
- background-repeat: repeat-x;
- border-color: #252525;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);
- background:none;
- color:black;
-
-}
-body {
- padding-top:0px;
-}
-
-a {
- cursor: pointer;
-}
\ No newline at end of file
diff --git a/public/css/static.styl b/public/css/static.styl
index fe576834e7..ff01b7f125 100644
--- a/public/css/static.styl
+++ b/public/css/static.styl
@@ -3,10 +3,38 @@
// Vendor Includes - include first so we can override
// Import only styles that do not have urls to images! Include them directly in the page!
@import "../bower_components/angular-loading-bar/build/loading-bar.css"
-
-@import "./static-pages.css"
@import "./shared.styl"
@import "./footer"
+body
+ margin-top: 60px
+
+#aboutPage
+ iframe
+ margin-bottom: 20px
+
.muted i, i.muted
- opacity:0.5
\ No newline at end of file
+ opacity:0.5
+
+#header-play-button
+ margin-right: 15px
+
+#tagline
+ margin:10px 0 20px 0
+ font-size:40px
+
+.marketing
+ text-align: center
+
+#frontpage-play-button
+ //box-shadow: 0 0 40px #494141;
+ display:inline
+ font-size: 20px
+ width:100px
+ height:50px
+
+#about-page
+ img
+ box-shadow 0px 0px 10px 5px #888
+ margin 10px
+ max-width 500px
\ No newline at end of file
diff --git a/public/css/tasks.styl b/public/css/tasks.styl
index 3e59280ac9..1391af1ecf 100644
--- a/public/css/tasks.styl
+++ b/public/css/tasks.styl
@@ -23,6 +23,8 @@ for $stage in $stages
.completed
background-color: $completed
color: #999
+ .glyphicon
+ color: #333
.task-checker label
background-color: darken($completed, 30%)
.task-action-btn
@@ -31,11 +33,20 @@ for $stage in $stages
.reward
background-color: white
-
-
// 2. Columns & Tasks
// ===================
+.tasks-lists
+
+ .row
+ margin-left: -5px
+ margin-right: -5px
+
+ .col-md-3
+ padding-left: 10px // Override Bootstrap 15px padding
+ padding-right: 10px // Override Bootstrap 15px padding
+ margin-bottom: 15px
+
// main columns
// ------------
.task-column
@@ -47,7 +58,13 @@ for $stage in $stages
.task-column_title
margin: 0 0 0.5em
padding: 0
+ font-weight: 300
+// 50% width columns with scrollbars for tablets
+@media (min-width: 768px) and (max-width: 970px)
+ .task-column
+ max-height: 18.6em
+ overflow-y: scroll
// add new task form
// -----------------
@@ -112,6 +129,9 @@ for $stage in $stages
&:last-child
margin-bottom: 0
+ label
+ font-weight: 400
+
// task content
.task-text
display: block
@@ -124,19 +144,14 @@ for $stage in $stages
height:1.5em
background-size:1.5em
-// task due date (for to-dos)
-.task-date
- font-size: 70%
-
.habit-wide .task-text
padding-left: 7em
// when a task is being dragged
-.task.ui-sortable-helper {
+.task.ui-sortable-helper
box-shadow: 0 0 3px rgba(0,0,0,0.15), 0 0 5px rgba(0,0,0,0.1)
transform: scale(1.05)
outline: 1px solid rgba(0,0,0,0.2)
-}
// primary task commands
// ----------------------
@@ -270,13 +285,23 @@ for $stage in $stages
// -----------------------
.task-meta-controls
float: right
- margin: 0.75em 0.5em 0 0.25em
+ margin: 0.75em 0.5em 0 0.5em
height: 1em
opacity: 0.25
- a > i
- margin-left: 0.125em
- .task-notes
- margin-left: 0.125em
+
+ a
+ text-decoration: none
+
+ // improve alignment of task icons
+ .glyphicon-signal
+ margin-left: 0.25em
+ .glyphicon-remove, .glyphicon-ok, a.badge
+ margin-right: 0.25em
+ .glyphicon-tags
+ margin-left: 0.1em
+ margin-right: 0.4em
+ a.badge
+ position: relative; top:-2px
.task:hover .task-meta-controls
opacity: 1
@@ -296,7 +321,6 @@ form
[class$="-options"]
padding: 1em 1em 0
color: #333
- z-index: 0
position: relative
//background-color:
@@ -360,6 +384,9 @@ form
text-align: center
margin: 0
+ &.task-attributes
+ text-align: left
+
.task-action-btn.tile
border: 0
font-size: 1.15em
@@ -393,6 +420,9 @@ form
&:first-child
border-right: 0
+.task-options
+ .task-action-btn.tile
+ margin: 3px
// todos ui
// --------
diff --git a/public/js/app.js b/public/js/app.js
index 914f0437f0..ef55bb10d4 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -1,23 +1,5 @@
"use strict";
-window.env = window.env || {}; //FIX tests
-
-if(window.env.language && window.env.language.momentLang && window.env.language.momentLangCode){
- var head = document.getElementsByTagName('head')[0];
- var script = document.createElement('script');
- script.type = 'text/javascript';
- script.text = window.env.language.momentLang;
- head.appendChild(script);
- moment.lang(window.env.language.momentLangCode);
-}
-
-window.env.t = function(stringName, vars){
- var string = window.env.translations[stringName];
- if(!string) return window._.template(window.env.translations.stringNotFound, {string: stringName});
-
- return vars === undefined ? string : window._.template(string, vars);
-}
-
window.habitrpg = angular.module('habitrpg',
['ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices',
'authServices', 'notificationServices', 'guideServices', 'authCtrl',
@@ -132,8 +114,12 @@ window.habitrpg = angular.module('habitrpg',
.state('options.social.guilds.detail', {
url: '/:gid',
templateUrl: 'partials/options.social.guilds.detail.html',
- controller: ['$scope', 'Groups', '$stateParams', function($scope, Groups, $stateParams){
- $scope.group = Groups.Group.get({gid:$stateParams.gid});
+ controller: ['$scope', 'Groups', '$stateParams',
+ function($scope, Groups, $stateParams){
+ Groups.Group.get({gid:$stateParams.gid}, function(group){
+ $scope.group = group;
+ Groups.seenMessage(group._id);
+ });
}]
})
@@ -218,24 +204,40 @@ window.habitrpg = angular.module('habitrpg',
}
// Handle errors
- var interceptor = ['$rootScope', '$q', function ($rootScope, $q) {
+ $httpProvider.responseInterceptors.push(['$rootScope', '$q', function ($rootScope, $q) {
function success(response) {
return response;
}
function error(response) {
- //var status = response.status;
- response.data = (response.data.err) ? response.data.err : response.data;
- if (response.status == 0) response.data = 'Server currently unreachable.';
- if (response.status == 500) response.data += ' (see Chrome console for more details).';
+ debugger;
+ // Offline
+ if (response.status == 0 ||
+ // don't know why we're getting 404 here, should be 0
+ (response.status == 404 && _.isEmpty(response.data))) {
+ $rootScope.$broadcast('responseText', window.env.t('serverUnreach'));
- var error = response.status == 0 ? response.data : ('Error ' + response.status + ': ' + response.data);
- $rootScope.$broadcast('responseError', error);
- console.log(arguments);
- return $q.reject(response);
+ // Needs refresh
+ } else if (response.needRefresh) {
+ $rootScope.$broadcast('responseError', "The site has been updated and the page needs to refresh. The last action has not been recorded, please refresh and try again.");
+
+ // 400 range?
+ } else if (response < 500) {
+ $rootScope.$broadcast('responseText', response.data.err || response.data);
+
+ // Error
+ } else {
+ var error = 'Please reload, ' +
+ '"'+window.env.t('error')+' '+(response.data.err || response.data || 'something went wrong')+'" ' +
+ window.env.t('seeConsole');
+ $rootScope.$broadcast('responseError', error);
+ console.error(response);
+ }
+
+ //return $q.reject(response); // this completely halts the chain, meaning we can't queue offline actions
+ return response;
}
return function (promise) {
return promise.then(success, error);
}
- }];
- $httpProvider.responseInterceptors.push(interceptor);
+ }]);
}])
diff --git a/public/js/controllers/authCtrl.js b/public/js/controllers/authCtrl.js
index 1c50d56be9..ab4b89eece 100644
--- a/public/js/controllers/authCtrl.js
+++ b/public/js/controllers/authCtrl.js
@@ -5,15 +5,15 @@
*/
angular.module('authCtrl', [])
- .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','API_URL',
- function($scope, $rootScope, User, $http, $location, $window, API_URL) {
+ .controller("AuthCtrl", ['$scope', '$rootScope', 'User', '$http', '$location', '$window','API_URL', '$modal',
+ function($scope, $rootScope, User, $http, $location, $window, API_URL, $modal) {
var runAuth;
var showedFacebookMessage;
$scope.useUUID = false;
$scope.toggleUUID = function() {
if (showedFacebookMessage === false) {
- alert("Until we add Facebook, use your UUID and API Token to log in (found at https://habitrpg.com > Options > Settings).");
+ alert(window.env.t('untilNoFace'));
showedFacebookMessage = true;
}
$scope.useUUID = !$scope.useUUID;
@@ -27,10 +27,19 @@ angular.module('authCtrl', [])
runAuth = function(id, token) {
User.authenticate(id, token, function(err) {
$window.location.href = '/';
- //$rootScope.modals.login = false;
});
};
+ function errorAlert(data, status, headers, config) {
+ if (status === 0) {
+ $window.alert(window.env.t('noReachServer'));
+ } else if (!!data && !!data.err) {
+ $window.alert(data.err);
+ } else {
+ $window.alert(window.env.t('errorUpCase') + ' ' + status);
+ }
+ };
+
$scope.register = function() {
/*TODO highlight invalid inputs
we have this as a workaround for https://github.com/HabitRPG/habitrpg-mobile/issues/64
@@ -40,27 +49,9 @@ angular.module('authCtrl', [])
}
$http.post(API_URL + "/api/v2/register", $scope.registerVals).success(function(data, status, headers, config) {
runAuth(data.id, data.apiToken);
- }).error(function(data, status, headers, config) {
- if (status === 0) {
- $window.alert("Server not currently reachable, try again later");
- } else if (!!data && !!data.err) {
- $window.alert(data.err);
- } else {
- $window.alert("ERROR: " + status);
- }
- });
+ }).error(errorAlert);
};
- function errorAlert(data, status, headers, config) {
- if (status === 0) {
- $window.alert("Server not currently reachable, try again later");
- } else if (!!data && !!data.err) {
- $window.alert(data.err);
- } else {
- $window.alert("ERROR: " + status);
- }
- }
-
$scope.auth = function() {
var data = {
username: $scope.loginUsername || $('#login-tab input[name="username"]').val(),
@@ -77,17 +68,21 @@ angular.module('authCtrl', [])
};
$scope.playButtonClick = function(){
+ window.ga && ga('send', 'event', 'button', 'click', 'Play');
if (User.authenticated()) {
window.location.href = '/#/tasks';
} else {
- $('#login-modal').modal('show');
+ $modal.open({
+ templateUrl: 'modals/login.html'
+ // Using controller: 'AuthCtrl' it causes problems
+ });
}
}
$scope.passwordReset = function(email){
$http.post(API_URL + '/api/v2/user/reset-password', {email:email})
.success(function(){
- alert('New password sent.');
+ alert(window.env.t('newPassSent'));
})
.error(function(data){
alert(data.err);
diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js
index 8c1cd05a57..979fa0e3a6 100644
--- a/public/js/controllers/challengesCtrl.js
+++ b/public/js/controllers/challengesCtrl.js
@@ -44,11 +44,11 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
* Save
*/
$scope.save = function(challenge) {
- if (!challenge.group) return alert('Please select group');
+ if (!challenge.group) return alert(window.env.t('selectGroup'));
var isNew = !challenge._id;
challenge.$save(function(_challenge){
if (isNew) {
- Notification.text('Challenge Created');
+ Notification.text(window.env.t('challengeCreated'));
$state.go('options.social.challenges.detail', {cid: _challenge._id});
$scope.discard();
$scope.challenges = Challenges.Challenge.query();
@@ -85,14 +85,14 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
$scope.closingChal = undefined;
}
$scope["delete"] = function(challenge) {
- if (!confirm("Delete challenge, are you sure?")) return;
+ if (!confirm(window.env.t('sureDelCha'))) return;
challenge.$delete(function(){
$scope.popoverEl.popover('destroy');
backToChallenges();
});
};
$scope.selectWinner = function(challenge) {
- if (!confirm("Are you sure?")) return;
+ if (!confirm(window.env.t('youSure'))) return;
challenge.$close({uid:challenge.winner}, function(){
$scope.popoverEl.popover('destroy');
backToChallenges();
@@ -106,7 +106,7 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
html: true,
placement: 'right',
trigger: 'manual',
- title: 'Close challenge and...',
+ title: window.env.t('closeCha'),
content: html
}).popover('show');
@@ -140,7 +140,7 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
};
$scope.removeTask = function(list, $index) {
- if (!confirm("Are you sure you want to delete this task?")) return;
+ if (!confirm(window.env.t('sureDelete'))) return;
//TODO persist
// User.log({op: "delTask", data: task});
list.splice($index, 1);
@@ -185,13 +185,13 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
$scope.selectedChal = chal;
$scope.popoverEl = $($event.target);
var html = $compile(
- 'Remove Tasks \nKeep Tasks \nCancel '
+ '' + window.env.t('removeTasks') + ' \n' + window.env.t('keepTasks') + ' \n' + window.env.t('cancel') + ' '
)($scope);
$scope.popoverEl.popover('destroy').popover({
html: true,
placement: 'top',
trigger: 'manual',
- title: 'Leave challenge and...',
+ title: window.env.t('leaveCha'),
content: html
}).popover('show');
}
diff --git a/public/js/controllers/footerCtrl.js b/public/js/controllers/footerCtrl.js
index 6541f6f70f..5d2fa238e6 100644
--- a/public/js/controllers/footerCtrl.js
+++ b/public/js/controllers/footerCtrl.js
@@ -1,6 +1,7 @@
"use strict";
-habitrpg.controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', 'Notification', 'API_URL',
+(typeof habitrpg !== 'undefined' ? habitrpg : habitrpgStatic)
+ .controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', 'Notification', 'API_URL',
function($scope, $rootScope, User, $http, Notification, API_URL) {
/**
@@ -13,17 +14,14 @@ habitrpg.controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', 'Not
// Stripe
$.getScript('//checkout.stripe.com/v2/checkout.js');
- // Amazon Affiliate
-// if ($rootScope.authenticated() && !User.user.purchased.ads) {
-// $.getScript('//wms.assoc-amazon.com/20070822/US/js/link-enhancer-common.js?tag=ha0d2-20').fail(function() {
-// $('body').append('');
-// });
-// }
-
// Google Analytics, only in production
if (window.env.NODE_ENV === 'production') {
- window._gaq = [["_setAccount", "UA-33510635-1"], ["_setDomainName", "habitrpg.com"], ["_trackPageview"]];
- $.getScript(("https:" === document.location.protocol ? "https://ssl" : "http://www") + ".google-analytics.com/ga.js");
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (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('send', 'pageview');
}
// Scripts only for desktop
@@ -44,27 +42,30 @@ habitrpg.controller("FooterCtrl", ['$scope', '$rootScope', 'User', '$http', 'Not
/**
* Debug functions. Note that the server route for gems is only available if process.env.DEBUG=true
*/
- $scope.addMissedDay = function(){
- if (!confirm("Are you sure you want to reset the day?")) return;
- var dayBefore = moment(User.user.lastCron).subtract('days', 1).toDate();
- User.set({'lastCron': dayBefore});
- Notification.text('-1 day, remember to refresh');
- }
- $scope.addTenGems = function(){
- $http.post(API_URL + '/api/v2/user/addTenGems').success(function(){
- User.log({});
- })
- }
- $scope.addLevelsAndGold = function(){
- User.set({
- 'stats.exp': User.user.stats.exp + 10000,
- 'stats.gp': User.user.stats.gp + 10000,
- 'stats.mp': User.user.stats.mp + 10000
- });
- }
- $scope.addOneLevel = function(){
- User.set({
- 'stats.exp': User.user.stats.exp + (Math.round(((Math.pow(User.user.stats.lvl, 2) * 0.25) + (10 * User.user.stats.lvl) + 139.75) / 10) * 10)
- });
+ if (window.env.NODE_ENV === 'development') {
+ $scope.addMissedDay = function(){
+ if (!confirm("Are you sure you want to reset the day?")) return;
+ var dayBefore = moment(User.user.lastCron).subtract('days', 1).toDate();
+ User.set({'lastCron': dayBefore});
+ Notification.text('-1 day, remember to refresh');
+ }
+ $scope.addTenGems = function(){
+ $http.post(API_URL + '/api/v2/user/addTenGems').success(function(){
+ User.log({});
+ })
+ }
+ $scope.addLevelsAndGold = function(){
+ User.set({
+ 'stats.exp': User.user.stats.exp + 10000,
+ 'stats.gp': User.user.stats.gp + 10000,
+ 'stats.mp': User.user.stats.mp + 10000
+ });
+ }
+ $scope.addOneLevel = function(){
+ User.set({
+ 'stats.exp': User.user.stats.exp + (Math.round(((Math.pow(User.user.stats.lvl, 2) * 0.25) + (10 * User.user.stats.lvl) + 139.75) / 10) * 10)
+ });
+ }
}
+
}])
\ No newline at end of file
diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js
index ddb36c824f..c0ba02c5a1 100644
--- a/public/js/controllers/groupsCtrl.js
+++ b/public/js/controllers/groupsCtrl.js
@@ -29,12 +29,12 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
// 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.modals.member = true;
+ $rootScope.openModal('member', {controller:'MemberModalCtrl'});
}
}
$scope.removeMember = function(group, member, isMember){
- var yes = confirm("Do you really want to remove this member from the party?")
+ var yes = confirm(window.env.t('sureKick'))
if(yes){
Groups.Group.removeMember({gid: group._id, uuid: member._id }, undefined, function(){
if(isMember){
@@ -99,8 +99,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.$watch('group.chat',$scope.chatChanged);
$scope.caretChanged = function(newCaretPos) {
- var relativeelement = $('.-options');
- var textarea = $('.chat-textarea');
+ var relativeelement = $('.chat-form div:first');
+ var textarea = $('.chat-form textarea');
var userlist = $('.list-at-user');
var offset = {
x: textarea.offset().left - relativeelement.offset().left,
@@ -180,7 +180,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.likeChatMessage = function(group,message) {
if (message.uuid == User.user._id)
- return Notification.text("Can't like your own message. Don't be that person.");
+ return Notification.text(window.env.t('foreverAlone'));
if (!message.likes) message.likes = {};
if (message.likes[User.user._id]) {
delete message.likes[User.user._id];
@@ -198,10 +198,10 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
// List of Ordering options for the party members list
$scope.partyOrderChoices = {
- 'level': 'Sort by Level',
- 'random': 'Sort randomly',
- 'pets': 'Sort by number of pets',
- 'party_date_joined': 'Sort by Party date joined',
+ 'level': window.env.t('sortLevel'),
+ 'random': window.env.t('sortRandom'),
+ 'pets': window.env.t('sortPets'),
+ 'party_date_joined': window.env.t('sortJoined'),
};
}])
@@ -219,9 +219,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}
$scope.newGroup = newGroup()
$scope.create = function(group){
- if (User.user.balance < 1) return $rootScope.modals.buyGems = true;
+ if (User.user.balance < 1) return $rootScope.openModal('buyGems', {track:"Gems > Create Group"});
- if (confirm("Create Guild for 4 Gems?")) {
+ if (confirm(window.env.t('confirmGuild'))) {
group.$save(function(saved){
User.user.balance--;
$scope.groups.guilds.push(saved);
@@ -284,14 +284,14 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
- 'Remove Tasks \nKeep Tasks \nCancel '
+ '' + window.env.t('removeTasks') + ' \n' + window.env.t('keepTasks') + ' \n' + window.env.t('cancel') + ' '
)($scope);
- title = "Leave group challenges and...";
+ title = window.env.t('leaveGroupCha');
} else {
html = $compile(
- 'Confirm \nCancel '
+ '' + window.env.t('confirm') + ' \n' + window.env.t('cancel') + ' '
)($scope);
- title = "Leave group?"
+ title = window.env.t('leaveGroup')
}
$scope.popoverEl.popover('destroy').popover({
html: true,
@@ -319,6 +319,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
$scope.text = 'Party';
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});
+
+ Groups.seenMessage($scope.group._id);
+
$scope.create = function(group){
group.$save(function(newGroup){
$scope.group = newGroup;
@@ -358,14 +361,14 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}), '_id');
if (_.intersection(challenges, User.user.challenges).length > 0) {
html = $compile(
- 'Remove Tasks \nKeep Tasks \nCancel '
+ '' + window.env.t('removeTasks') + ' \n' + window.env.t('keepTasks') + ' \n' + window.env.t('cancel') + ' '
)($scope);
- title = "Leave party challenges and...";
+ title = window.env.t('leavePartyCha');
} else {
html = $compile(
- 'Confirm \nCancel '
+ '' + window.env.t('confirm') + ' \n' + window.env.t('cancel') + ' '
)($scope);
- title = "Leave party?";
+ title = window.env.t('leaveParty');
}
$scope.popoverEl.popover('destroy').popover({
html: true,
@@ -383,8 +386,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
}
$scope.questAbort = function(){
- if (!confirm("Are you sure you want to abort this mission? It will abort it for everyone in your party, and you'll lose your quest scroll.")) return;
- if (!confirm("Are you double sure? Make sure they won't hate you forever!")) return;
+ if (!confirm(window.env.t('sureAbort'))) return;
+ if (!confirm(window.env.t('doubleSureAbort'))) return;
$rootScope.party.$questAbort();
}
}
diff --git a/public/js/controllers/inventoryCtrl.js b/public/js/controllers/inventoryCtrl.js
index 6d769ca47f..8b3b084903 100644
--- a/public/js/controllers/inventoryCtrl.js
+++ b/public/js/controllers/inventoryCtrl.js
@@ -7,7 +7,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
$scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5}
$scope.selectedPotion = null; // {index: 5, name: "Red", value: 3}
- $scope.totalPets = _.size(Content.eggs) * _.size(Content.hatchingPotions);
+ $scope.totalPets = _.size(Content.dropEggs) * _.size(Content.hatchingPotions);
+ $scope.totalMounts = _.size(Content.eggs) * _.size(Content.hatchingPotions);
// count egg, food, hatchingPotion stack totals
var countStacks = function(items) { return _.reduce(items,function(m,v){return m+v;},0);}
@@ -79,7 +80,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
}
$scope.hatch = function(egg, potion){
- if (!$window.confirm('Hatch a ' + potion.key + ' ' + egg.key + '?')) return;
+ if (!$window.confirm(window.env.t('hatchAPot', {potion: potion.key, egg: egg.key}))) return;
user.ops.hatch({params:{egg:egg.key, hatchingPotion:potion.key}});
$scope.selectedEgg = null;
$scope.selectedPotion = null;
@@ -87,9 +88,11 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
$scope.purchase = function(type, item){
var gems = User.user.balance * 4;
- if(gems < item.value) return $rootScope.modals.buyGems = true;
- var string = (type == 'hatchingPotion') ? 'hatching potion' : type; // give hatchingPotion a space
- var message = "Buy this " + string + " with " + item.value + " of your " + gems + " Gems?"
+
+ if(gems < item.value) return $rootScope.openModal('buyGems');
+ var string = (type == 'hatchingPotions') ? 'hatching potion' : (type == 'eggs') ? 'egg' : (type == 'quests') ? 'quest' : (item.key == 'Saddle') ? 'saddle' : (type == 'special') ? item.key : type; // this is ugly but temporary, once the purchase modal is done this will be removed
+ var message = window.env.t('buyThis', {text: string, price: item.value, gems: gems})
+
if($window.confirm(message))
User.user.ops.purchase({params:{type:type,key:item.key}});
}
@@ -102,8 +105,8 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
if ($scope.selectedFood) {
var food = $scope.selectedFood
if (food.key == 'Saddle') {
- if (!$window.confirm('Saddle ' + pet + '?')) return;
- } else if (!$window.confirm('Feed ' + petDisplayName + ' '+ food.article + food.text + '?')) {
+ if (!$window.confirm(window.env.t('useSaddle', {pet: pet}))) return;
+ } else if (!$window.confirm(window.env.t('feedPet', {name: petDisplayName, article: food.article, text: food.text}))) {
return;
}
User.user.ops.feed({params:{pet: pet, food: food.key}});
@@ -123,16 +126,14 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
var item = Content.quests[quest];
var completedPrevious = !item.previous || (User.user.achievements.quests && User.user.achievements.quests[item.previous]);
if (!completedPrevious)
- return alert("You must first complete " + $rootScope.Content.quests[item.previous].text + '.');
+ return alert(window.env.t('mustComplete', {quest: $rootScope.Content.quests[item.previous].text}));
if (item.lvl && item.lvl > user.stats.lvl)
- return alert("You must be level " + item.lvl + '.');
+ return alert(window.env.t('mustLevel', {level: item.lvl}));
$rootScope.selectedQuest = item;
- $rootScope.modals.showQuest = true;
+ $rootScope.openModal('showQuest', {controller:'InventoryCtrl'});
}
$scope.closeQuest = function(){
$rootScope.selectedQuest = undefined;
- $rootScope.modals.showQuest = false;
- $rootScope.modals.buyQuest = false;
}
$scope.questInit = function(){
$rootScope.party.$questAccept({key:$scope.selectedQuest.key}, function(){
@@ -142,11 +143,13 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
}
$scope.buyQuest = function(quest) {
var item = Content.quests[quest];
+ if (item.lvl && item.lvl > user.stats.lvl)
+ return alert(window.env.t('mustLvlQuest', {level: item.lvl}));
var completedPrevious = !item.previous || (User.user.achievements.quests && User.user.achievements.quests[item.previous]);
if (!completedPrevious)
return $scope.purchase("quests", item);
$rootScope.selectedQuest = item;
- $rootScope.modals.buyQuest = true;
+ $rootScope.openModal('buyQuest', {controller:'InventoryCtrl'});
}
}
]);
diff --git a/public/js/controllers/notificationCtrl.js b/public/js/controllers/notificationCtrl.js
index a93deb9b1a..82c42af8a7 100644
--- a/public/js/controllers/notificationCtrl.js
+++ b/public/js/controllers/notificationCtrl.js
@@ -1,9 +1,13 @@
'use strict';
habitrpg.controller('NotificationCtrl',
- ['$scope', '$rootScope', 'Shared', 'User', 'Guide', 'Notification', function ($scope, $rootScope, Shared, User, Guide, Notification) {
+ ['$scope', '$rootScope', 'Shared', 'User', 'Guide', 'Notification',
+ function ($scope, $rootScope, Shared, User, Guide, Notification) {
$rootScope.$watch('user.stats.hp', function(after, before) {
+ if (after <= 0){
+ $rootScope.openModal('death', {keyboard:false, backdrop:'static'});
+ }
if (after == before) return;
if (User.user.stats.lvl == 0) return;
Notification.hp(after - before, 'hp');
@@ -26,7 +30,7 @@ habitrpg.controller('NotificationCtrl',
if ((money > 0) && !!bonus) {
if (bonus < 0.01) bonus = 0.01;
- Notification.text("+ " + Notification.coins(bonus) + " Streak Bonus!");
+ Notification.text("+ " + Notification.coins(bonus) + ' ' + window.env.t('streakCoins'));
delete User.user._tmp.streakBonus;
}
});
@@ -61,31 +65,35 @@ habitrpg.controller('NotificationCtrl',
$rootScope.$watch('user.achievements.streak', function(after, before){
if(before == undefined || after == before || after < before) return;
- $rootScope.modals.achievements.streak = true;
+ $rootScope.openModal('achievements/streak');
});
$rootScope.$watch('user.achievements.ultimateGear', function(after, before){
if (after === before || after !== true) return;
- $rootScope.modals.achievements.ultimateGear = true;
+ $rootScope.openModal('achievements/ultimateGear');
});
$rootScope.$watch('user.items.pets', function(after, before){
if(_.size(after) === _.size(before) ||
Shared.countPets(null, after) < 90) return;
User.user.achievements.beastMaster = true;
- $rootScope.modals.achievements.beastMaster = true;
+ $rootScope.openModal('achievements/beastMaster');
}, true);
$rootScope.$watch('user.achievements.rebirths', function(after, before){
if(after === before) return;
- $rootScope.modals.achievements.rebirth = true;
+ $rootScope.openModal('achievements/rebirth');
+ });
+
+ $rootScope.$watch('user.flags.contributor', function(after, before){
+ if (after === before || after !== true) return;
+ $rootScope.openModal('achievements/contributor');
});
/*_.each(['weapon', 'head', 'chest', 'shield'], function(watched){
$rootScope.$watch('user.items.' + watched, function(before, after){
if (after == before) return;
if (+after < +before) {
- Notification.death();
//don't want to day "lost a head"
if (watched === 'head') watched = 'helm';
Notification.text('Lost GP, 1 LVL, ' + watched);
@@ -93,6 +101,13 @@ habitrpg.controller('NotificationCtrl',
})
});*/
+ // Classes modal
+ $rootScope.$watch('!user.flags.classSelected && user.stats.lvl >= 10', function(after, before){
+ if(after){
+ $rootScope.openModal('chooseClass', {controller:'UserCtrl', keyboard:false, backdrop:'static'});
+ }
+ });
+
$rootScope.$watch('user.stats.lvl', function(after, before) {
if (after == before) return;
if (after > before) {
@@ -100,8 +115,23 @@ habitrpg.controller('NotificationCtrl',
}
});
+ // Completed quest modal
+ $rootScope.$watch('user.party.quest.completed', function(after, before){
+ if (after == before || after != true) return;
+ $rootScope.openModal('questCompleted', {controller:'InventoryCtrl'});
+ });
+
+ // Quest invitation modal
+ $rootScope.$watch('party.quest.key && !party.quest.active && party.quest.members[user._id] == undefined', function(after, before){
+ if (after == before || after != true) return;
+ $rootScope.openModal('questInvitation');
+ });
+
$rootScope.$on('responseError', function(ev, error){
Notification.error(error);
});
+ $rootScope.$on('responseText', function(ev, error){
+ Notification.text(error);
+ });
}
]);
diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js
index e37a4a9662..508afbdf33 100644
--- a/public/js/controllers/rootCtrl.js
+++ b/public/js/controllers/rootCtrl.js
@@ -3,8 +3,8 @@
/* Make user and settings available for everyone through root scope.
*/
-habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content',
- function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content) {
+habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', 'Notification', 'Groups', 'Shared', 'Content', '$modal', '$timeout',
+ function($scope, $rootScope, $location, User, $http, $state, $stateParams, Notification, Groups, Shared, Content, $modal, $timeout) {
var user = User.user;
var initSticky = _.once(function(){
@@ -13,8 +13,11 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
})
$rootScope.$on('userUpdated',initSticky);
- $rootScope.modals = {};
- $rootScope.modals.achievements = {};
+ $rootScope.$on('$stateChangeSuccess',
+ function(event, toState, toParams, fromState, fromParams){
+ if (!!fromState.name) window.ga && ga('send', 'pageview', {page: '/#/'+toState.name});
+ });
+
$rootScope.User = User;
$rootScope.user = user;
$rootScope.moment = window.moment;
@@ -23,6 +26,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$rootScope.Shared = Shared;
$rootScope.Content = Content;
$rootScope.env = window.env;
+ $rootScope.Math = Math;
// Angular UI Router
$rootScope.$state = $state;
@@ -66,29 +70,60 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$rootScope.set = User.set;
$rootScope.authenticated = User.authenticated;
+ // Open a modal from a template expression (like ng-click,...)
+ // Otherwise use the proper $modal.open
+ $rootScope.openModal = function(template, options){//controller, scope, keyboard, backdrop){
+ if (!options) options = {};
+ if (options.track) window.ga && ga('send', 'event', 'button', 'click', options.track);
+ return $modal.open({
+ templateUrl: 'modals/' + template + '.html',
+ controller: options.controller, // optional
+ scope: options.scope, // optional
+ keyboard: (options.keyboard === undefined ? true : options.keyboard), // optional
+ backdrop: (options.backdrop === undefined ? true : options.backdrop) // optional
+ });
+ }
+
$rootScope.dismissAlert = function() {
- $rootScope.modals.newStuff = false;
$rootScope.set({'flags.newStuff':false});
}
$rootScope.notPorted = function(){
- alert("This feature is not yet ported from the original site.");
+ alert(window.env.t('notPorted'));
}
$rootScope.dismissErrorOrWarning = function(type, $index){
$rootScope.flash[type].splice($index, 1);
}
+ $rootScope.initPayPalButton = function($event){
+ //debugger
+ var data = {
+ name:env.t('paypalText'),
+ //env:window.env.NODE_ENV == 'production' ? '' : 'sandbox',
+ quantity:1,
+ amount:5,
+ currency:'USD',
+ tax:0,
+ callback:window.env.BASE_URL + '/api/v2/user/buy-gems/paypal-ipn',
+ custom:'?uid='+User.user._id + '&apiToken=' + User.user.apiToken,
+ 'return':window.env.BASE_URL,
+ rm:1,
+ no_shipping:1
+ };
+ PAYPAL.apps.ButtonFactory.create(window.env.PAYPAL_MERCHANT, data, 'buynow', document.getElementById('custom-paypal-button'));
+ }
+
$rootScope.showStripe = function(subscription) {
StripeCheckout.open({
key: window.env.STRIPE_PUB_KEY,
address: false,
amount: 500,
- name: subscription ? "Subscribe" : "Checkout",
+ name: subscription ? window.env.t('subscribe') : window.env.t('checkout'),
description: subscription ?
- "Buy gems with Gold, No Ads, Support the Devs" :
- "20 Gems, No Ads, Support the Devs",
- panelLabel: subscription ? "Subscribe" : "Checkout",
+ window.env.t('buySubsText') :
+ window.env.t('buyCheckText'),
+ panelLabel: subscription ? window.env.t('subscribe') : window.env.t('checkout'),
token: function(data) {
var url = '/api/v2/user/buy-gems';
if (subscription) url += '?plan=basic_earned';
@@ -105,7 +140,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
}
$scope.cancelSubscription = function(){
- if (!confirm("Are you sure you want to cancel your subscription?")) return;
+ if (!confirm(window.env.t('sureCancelSub'))) return;
//TODO use Stripe API to keep subscription till end of their month
$http.post('/api/v2/user/cancel-subscription').success(function(){
window.location.reload(true);
@@ -117,7 +152,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
if (backer && backer.npc) return backer.npc;
var l = contrib && contrib.level;
if (l && l > 0) {
- var level = (l < 3) ? 'Friend' : (l < 5) ? 'Elite' : (l < 7) ? 'Champion' : (l < 8) ? 'Legendary' : 'Heroic';
+ var level = (l < 3) ? window.env.t('friend') : (l < 5) ? window.env.t('elite') : (l < 7) ? window.env.t('champion') : (l < 8) ? window.env.t('legendary') : window.env.t('heroic');
return level + ' ' + contrib.text;
}
}
@@ -145,7 +180,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
});
data = google.visualization.arrayToDataTable(matrix);
options = {
- title: 'History',
+ title: window.env.t('history'),
backgroundColor: {
fill: 'transparent'
},
@@ -163,7 +198,12 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
------------------------
*/
$scope.castStart = function(spell) {
- if (User.user.stats.mp < spell.mana) return Notification.text("Not enough mana.");
+ if (User.user.stats.mp < spell.mana) return Notification.text(window.env.t('notEnoughMana'));
+
+ // Temporary for valentine's day, remove after event
+ if (spell.key == 'valentine' && User.user.stats.gp < spell.value)
+ return Notification.text('Not enough gold.');
+
$rootScope.applyingAction = true;
$scope.spell = spell;
if (spell.target == 'self') {
@@ -178,7 +218,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$scope.castEnd = function(target, type, $event){
if (!$rootScope.applyingAction) return;
$event && ($event.stopPropagation(),$event.preventDefault());
- if ($scope.spell.target != type) return Notification.text("Invalid target");
+ if ($scope.spell.target != type) return Notification.text(window.env.t('invalidTarget'));
$scope.spell.cast(User.user, target);
User.save();
@@ -189,11 +229,11 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
$http.post('/api/v2/user/class/cast/'+spell.key+'?targetType='+type+'&targetId='+targetId)
.success(function(){
- var msg = "You cast " + spell.text;
+ var msg = window.env.t('youCast', {spell: spell.text});
switch (type) {
- case 'task': msg += ' on ' + target.text + '.';break;
- case 'user': msg += ' on ' + target.profile.name + '.';break;
- case 'party': msg += ' for the Party.';break;
+ case 'task': msg = window.env.t('youCastTarget', {spell: spell.text, target: target.text});break;
+ case 'user': msg = window.env.t('youCastTarget', {spell: spell.text, target: target.profile.name});break;
+ case 'party': msg = window.env.t('youCastParty', {spell: spell.text});break;
}
Notification.text(msg);
});
diff --git a/public/js/controllers/settingsCtrl.js b/public/js/controllers/settingsCtrl.js
index 8ad6419a14..98752e50be 100644
--- a/public/js/controllers/settingsCtrl.js
+++ b/public/js/controllers/settingsCtrl.js
@@ -38,7 +38,7 @@ habitrpg.controller('SettingsCtrl',
var dayStart = +User.user.preferences.dayStart;
if (_.isNaN(dayStart) || dayStart < 0 || dayStart > 24) {
dayStart = 0;
- return alert('Please enter a number between 0 and 24');
+ return alert(window.env.t('enterNumber'));
}
User.set({'preferences.dayStart': dayStart});
}
@@ -55,23 +55,21 @@ habitrpg.controller('SettingsCtrl',
$scope.reroll = function(){
User.user.ops.reroll({});
- $rootScope.modals.reroll = false;
$rootScope.$state.go('tasks');
}
$scope.rebirth = function(){
User.user.ops.rebirth({});
- $rootScope.modals.rebirth = false;
$rootScope.$state.go('tasks');
}
$scope.changePassword = function(changePass){
if (!changePass.oldPassword || !changePass.newPassword || !changePass.confirmNewPassword) {
- return alert("Please fill out all fields");
+ return alert(window.env.t('fillAll'));
}
$http.post(API_URL + '/api/v2/user/change-password', changePass)
.success(function(){
- alert("Password successfully changed");
+ alert(window.env.t('passSuccess'));
$scope.changePass = {};
})
.error(function(data){
@@ -80,12 +78,11 @@ habitrpg.controller('SettingsCtrl',
}
$scope.restoreValues = {};
- $rootScope.$watch('modals.restore', function(value){
- if(value === true){
- $scope.restoreValues.stats = angular.copy(User.user.stats);
- $scope.restoreValues.achievements = {streak: User.user.achievements.streak || 0};
- }
- })
+ $rootScope.openRestoreModal = function(){
+ $scope.restoreValues.stats = angular.copy(User.user.stats);
+ $scope.restoreValues.achievements = {streak: User.user.achievements.streak || 0};
+ $rootScope.openModal('restore', {scope:$scope});
+ };
$scope.restore = function(){
var stats = $scope.restoreValues.stats,
@@ -98,12 +95,10 @@ habitrpg.controller('SettingsCtrl',
"stats.mp": stats.mp,
"achievements.streak": achievements.streak
});
- $rootScope.modals.restore = false;
}
$scope.reset = function(){
User.user.ops.reset({});
- $rootScope.modals.reset = false;
$rootScope.$state.go('tasks');
}
diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js
index 5dd8fc9369..1bde5c0bed 100644
--- a/public/js/controllers/tasksCtrl.js
+++ b/public/js/controllers/tasksCtrl.js
@@ -38,7 +38,7 @@ habitrpg.controller("TasksCtrl", ['$scope', '$location', 'User','Notification',
};
$scope.removeTask = function(list, $index) {
- if (!confirm("Are you sure you want to delete this task?")) return;
+ if (!confirm(window.env.t('sureDelete'))) return;
User.user.ops.deleteTask({params:{id:list[$index].id}})
};
diff --git a/public/js/controllers/userCtrl.js b/public/js/controllers/userCtrl.js
index ce3c629d82..43c1996296 100644
--- a/public/js/controllers/userCtrl.js
+++ b/public/js/controllers/userCtrl.js
@@ -18,7 +18,7 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$
$scope.changeClass = function(klass){
if (!klass) {
- if (!confirm("Are you sure? This will reset your character's class and allocated points (you'll get them all back to re-allocate), and costs 3 gems"))
+ if (!confirm(window.env.t('sureReset')))
return;
return User.user.ops.changeClass({});
}
@@ -52,11 +52,11 @@ habitrpg.controller("UserCtrl", ['$rootScope', '$scope', '$location', 'User', '$
var cost = fullSet ? 1.25 : 0.5; // 5G per set, 2G per individual
if (fullSet) {
- if (confirm("Purchase for 5 Gems?") !== true) return;
- if (User.user.balance < cost) return $rootScope.modals.buyGems = true;
+ if (confirm(window.env.t('purchaseFor5')) !== true) return;
+ if (User.user.balance < cost) return $rootScope.openModal('buyGems');
} else if (!User.user.fns.dotGet('purchased.' + path)) {
- if (confirm("Purchase for 2 Gems?") !== true) return;
- if (User.user.balance < cost) return $rootScope.modals.buyGems = true;
+ if (confirm(window.env.t('purchaseFor2')) !== true) return;
+ if (User.user.balance < cost) return $rootScope.openModal('buyGems');
}
User.user.ops.unlock({query:{path:path}})
}
diff --git a/public/js/directives/directives.js b/public/js/directives/directives.js
index 2d36fea375..7a8e9d71fd 100644
--- a/public/js/directives/directives.js
+++ b/public/js/directives/directives.js
@@ -26,7 +26,7 @@ habitrpg.directive('habitrpgAdsense', function() {
template: '
',
link: function ($scope, element, attrs) {}
}
-})
+});
habitrpg.directive('whenScrolled', function() {
return function(scope, elm, attr) {
@@ -85,21 +85,21 @@ habitrpg
scope.main = attrs.main;
$rootScope.lists = [
{
- header: env.t('habits'),
+ header: window.env.t('habits'),
type: 'habit',
- placeHolder: env.t('newHabit')
+ placeHolder: window.env.t('newHabit')
}, {
- header: env.t('dailies'),
+ header: window.env.t('dailies'),
type: 'daily',
- placeHolder: env.t('newDaily')
+ placeHolder: window.env.t('newDaily')
}, {
- header: env.t('todos'),
+ header: window.env.t('todos'),
type: 'todo',
- placeHolder: env.t('newTodo')
+ placeHolder: window.env.t('newTodo')
}, {
- header: env.t('rewards'),
+ header: window.env.t('rewards'),
type: 'reward',
- placeHolder: env.t('newReward')
+ placeHolder: window.env.t('newReward')
}
];
diff --git a/public/js/env.js b/public/js/env.js
new file mode 100644
index 0000000000..8a6611cd90
--- /dev/null
+++ b/public/js/env.js
@@ -0,0 +1,20 @@
+"use strict";
+
+window.env = window.env || {}; //FIX tests
+
+// If Moment.js is loaded,
+if(window.moment && window.env.language && window.env.language.momentLang && window.env.language.momentLangCode){
+ var head = document.getElementsByTagName('head')[0];
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.text = window.env.language.momentLang;
+ head.appendChild(script);
+ window.moment.lang(window.env.language.momentLangCode);
+}
+
+window.env.t = function(stringName, vars){
+ var string = window.env.translations[stringName];
+ if(!string) return window._.template(window.env.translations.stringNotFound, {string: stringName});
+
+ return vars === undefined ? string : window._.template(string, vars);
+}
\ No newline at end of file
diff --git a/public/js/services/authServices.js b/public/js/services/authServices.js
index d1667c959a..9e55d7af32 100644
--- a/public/js/services/authServices.js
+++ b/public/js/services/authServices.js
@@ -32,7 +32,7 @@ factory('Facebook',
$http.post(API_URL + '/api/v2/user/auth/facebook', data).success(function(data, status, headers, config) {
User.authenticate(data.id, data.token, function(err) {
if (!err) {
- alert('Login successful!');
+ alert(window.env.t('loginSuccess'));
$location.path("/habit");
}
});
diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js
index 1f72b4a648..db33d73de9 100644
--- a/public/js/services/groupServices.js
+++ b/public/js/services/groupServices.js
@@ -5,8 +5,8 @@
*/
angular.module('groupServices', ['ngResource']).
- factory('Groups', ['API_URL', '$resource', '$q',
- function(API_URL, $resource, $q) {
+ factory('Groups', ['API_URL', '$resource', '$q', '$http', 'User',
+ function(API_URL, $resource, $q, $http, User) {
var Group = $resource(API_URL + '/api/v2/groups/:gid',
{gid:'@_id', messageId: '@_messageId'},
{
@@ -44,6 +44,12 @@ angular.module('groupServices', ['ngResource']).
return tavern;
},
+ // On enter, set chat message to "seen"
+ seenMessage: function(gid){
+ $http.post('/api/v2/groups/'+gid+'/chat/seen');
+ if (User.user.newMessages) User.user.newMessages[gid] = false;
+ },
+
Group: Group
}
}
diff --git a/public/js/services/guideServices.js b/public/js/services/guideServices.js
index 32df74a401..98879e040b 100644
--- a/public/js/services/guideServices.js
+++ b/public/js/services/guideServices.js
@@ -18,58 +18,58 @@ angular.module('guideServices', []).
var tourSteps = [
{
element: ".main-herobox",
- title: "Welcome to HabitRPG",
- content: "Welcome to HabitRPG, a habit-tracker which treats your goals like a Role Playing Game. I'm Justin, your guide!",
+ title: window.env.t('welcomeHabit'),
+ content: window.env.t('welcomeHabitT1') + " Justin, " + window.env.t('welcomeHabitT2'),
}, {
element: ".main-herobox",
- title: "Your Avatar",
- content: "This is your avatar. It represents you in the world of Habitica. As you accomplish goals, your avatar will gain levels, earn gold, and equip itself for further challenges ahead.",
+ title: window.env.t('yourAvatar'),
+ content: window.env.t('yourAvatarText'),
}, {
element: ".main-herobox",
- title: "Avatar Customization",
- content: "You can customize your avatar by clicking anywhere in this box. Change your body type, hair color, skin color, and more from this menu. You can also find a number of HabitRPG's exciting social features by clicking through tabs on the customization page.",
+ title: window.env.t('avatarCustom'),
+ content: window.env.t('avatarCustomText'),
}, {
element: "#bars",
- title: "Hit Points",
- content: "The red bar tracks your avatar's health points. Whenever you fail to meet a goal, you take damage and lose health. If your health bar reaches zero, you die. Dying results in the loss of one level, all your gold, and a piece of equipment.",
+ title: window.env.t('hitPoints'),
+ content: window.env.t('hitPointsText'),
}, {
element: "#bars",
- title: "Experience Points",
- content: "The yellow bar tracks your avatar's experience points. Whenever you succeed in achieving a goal, you gain both gold and experience. When your experience bar maxes out, you gain a level. Gaining levels is how you unlock new and exciting features on HabitRPG.",
+ title: window.env.t('expPoints'),
+ content: window.env.t('expPointsText'),
}, {
element: "ul.habits",
- title: "Types of Goals",
- content: "HabitRPG allows you to track your goals in three different ways. These goals are categorized in columns as Habits, Dailies, or To-Dos.",
+ title: window.env.t('typeGoals'),
+ content: window.env.t('typeGoalsText'),
placement: "top"
}, {
element: "ul.habits",
- title: "Habits",
- content: "Habits are goals that you constantly track. They can be given plus or minus values, allowing you to gain experience and gold for good habits or lose health for bad ones.",
+ title: window.env.t('habits'),
+ content: window.env.t('tourHabits'),
placement: "top"
}, {
element: "ul.dailys",
- title: "Dailies",
- content: "Dailies are goals that you want to complete once a day. Checking off a daily reaps experience and gold. Failing to check off your daily before the day resets results in a loss of health. You can change your day start settings from the options menu.",
+ title: window.env.t('dailies'),
+ content: window.env.t('tourDailies'),
placement: "top"
}, {
element: "ul.todos",
- title: "To-Dos",
- content: "To-Dos are one-off goals that you can get to eventually. While it is possible to set a deadline on a to-do, they are not required. To-Dos make for a quick and easy way to gain experience.",
+ title: window.env.t('todos'),
+ content: window.env.t('tourTodos'),
placement: "top"
}, {
element: "ul.main-list.rewards",
- title: "Rewards",
- content: "All that gold you earned will allow you to reward yourself with either custom or in-game prizes. Buy them liberally – rewarding yourself is integral in forming good habits.",
+ title: window.env.t('rewards'),
+ content: window.env.t('tourRewards'),
placement: "top"
}, {
element: "ul.habits li:first-child",
- title: "Hover over comments",
- content: "You can add comments to your tasks by clicking the edit icon. Hover over each task's comment for more details about how HabitRPG works. When you're ready to get started, you can delete the existing tasks and add your own.",
+ title: window.env.t('hoverOver'),
+ content: window.env.t('hoverOverText'),
placement: "right"
}, {
element: "ul.habits li:first-child",
- title: "Unlock New Features",
- content: "That's all you need to know for now, but I'll be back as you level up to let you know about new features you've unlocked. Each new feature will give you more incentives to accomplish your goals. Find out more at the HabitRPG Wiki or let yourself be surprised. Good luck!",
+ title: window.env.t('unlockFeatures'),
+ content: window.env.t('unlockFeaturesT1') + " " + window.env.t('habitWiki') + " " + window.env.t('unlockFeaturesT2'),
placement: "right"
}
];
@@ -96,7 +96,7 @@ angular.module('guideServices', []).
var showPopover = function(selector, title, html, placement) {
if (!placement) placement = 'bottom';
$(selector).popover('destroy');
- var button = "";
+ var button = "";
html = "
" + html + ' ' + button + '
';
$(selector).popover({
title: title,
@@ -109,19 +109,19 @@ angular.module('guideServices', []).
$rootScope.$watch('user.flags.customizationsNotification', function(after, before) {
if (alreadyShown(before, after)) return;
- showPopover('.main-herobox', 'Customize Your Avatar', "Click your avatar to customize your appearance.", 'bottom');
+ showPopover('.main-herobox', window.env.t('customAvatar'), window.env.t('customAvatarText'), 'bottom');
});
$rootScope.$watch('user.flags.itemsEnabled', function(after, before) {
if (alreadyShown(before, after)) return;
- var html = "Congratulations, you have unlocked the Item Store! You can now buy weapons, armor, potions, etc. Read each item's comment for more information.";
- showPopover('div.rewards', 'Item Store Unlocked', html, 'left');
+ var html = window.env.t('storeUnlockedText');
+ showPopover('div.rewards', window.env.t('storeUnlocked'), html, 'left');
});
$rootScope.$watch('user.flags.partyEnabled', function(after, before) {
if (alreadyShown(before, after)) return;
- var html = "Be social, join a party and play Habit with your friends! You'll be better at your habits with accountability partners. Click User -> Options -> Party, and follow the instructions. LFG anyone?";
- showPopover('.user-menu', 'Party System', html, 'bottom');
+ var html = window.env.t('partySysText');
+ showPopover('.user-menu', window.env.t('partySys'), html, 'bottom');
});
$rootScope.$watch('user.flags.dropsEnabled', function(after, before) {
@@ -130,20 +130,12 @@ angular.module('guideServices', []).
if (!eggs) {
eggs['Wolf'] = 1; // This is also set on the server
}
- $rootScope.modals.dropsEnabled = true;
- });
-
- $rootScope.$watch('user.items.pets', function(after, before) {
- if (User.user.achievements && User.user.achievements.beastMaster) return;
- if (before >= 90) {
- User.set({'achievements.beastMaster': true});
- $('#beastmaster-achievement-modal').modal('show'); // FIXME
- }
+ $rootScope.openModal('dropsEnabled');
});
$rootScope.$watch('user.flags.rebirthEnabled', function(after, before) {
if (alreadyShown(before, after)) return;
- $rootScope.modals.rebirthEnabled = true;
+ $rootScope.openModal('rebirthEnabled');
});
@@ -160,8 +152,8 @@ angular.module('guideServices', []).
$timeout(function(){tour.goTo(0)});
}),
element: '.equipment-tab',
- title: "Class Gear",
- content: "First: don't panic! Your old gear is in your inventory, and you're now wearing your apprentice " + User.user.stats.class + " equipment. Wearing your class's gear grants you a 50% bonus to stats. However, feel free to switch back to your old gear."
+ title: window.env.t('classGear'),
+ content: window.env.t('classGearText', {klass: User.user.stats.class})
},
{
path: '/#/options/profile/stats',
@@ -169,21 +161,21 @@ angular.module('guideServices', []).
$timeout(function(){tour.goTo(1)});
}),
element: ".allocate-stats",
- title: "Stats",
- content: "These are your class's stats, they affect the game-play. Each time you level up, you get one point to allocate to particular stat. Hover over each stat for more information.",
+ title: window.env.t('stats'),
+ content: window.env.t('classStats'),
}, {
element: ".auto-allocate",
- title: "Auto Allocate",
+ title: window.env.t('autoAllocate'),
placement: 'left',
- content: "If 'automatic allocation' is checked, your avatar gains stats automatically based on your tasks' attributes, which you can find in TASK > Edit > Advanced > Attributes. Eg, if you hit the gym often, and your 'Gym' Daily is set to 'Physical', you'll gain STR automatically.",
+ content: window.env.t('autoAllocateText'),
}, {
element: ".meter.mana",
- title: "Spells",
- content: "You can now unlock class-specific spells. You'll see your first at level 11. Your mana replenishes 10 points per day, plus 1 point per completed To-Do."
+ title: window.env.t('spells'),
+ content: window.env.t('spellsText') + " " + window.env.t('toDo') + "."
}, {
orphan: true,
- title: "Read More",
- content: "For more information on the class-system, see Wikia."
+ title: window.env.t('readMore'),
+ content: window.env.t('moreClass') + " Wikia."
}
];
_.each(tourSteps, function(step){
diff --git a/public/js/services/notificationServices.js b/public/js/services/notificationServices.js
index 7b4a48c817..c67770e8f0 100644
--- a/public/js/services/notificationServices.js
+++ b/public/js/services/notificationServices.js
@@ -6,11 +6,11 @@ angular.module("notificationServices", [])
function growl(html, type) {
$.bootstrapGrowl(html, {
ele: '#notification-area',
- type: type, //(null, 'text', 'error', 'success', 'gp', 'xp', 'hp', 'lvl', 'death', 'mp', 'crit')
+ type: type || 'warning', //('info', 'text', 'warning', 'success', 'gp', 'xp', 'hp', 'lvl', 'death', 'mp', 'crit')
top_offset: 20,
align: 'right', //('left', 'right', or 'center')
width: 250, //(integer, or 'auto')
- delay: 7000,
+ delay: (type=='error') ? 0 : 7000,
allow_dismiss: true,
stackup_spacing: 10 // spacing between consecutive stacecked growls.
});
@@ -25,11 +25,11 @@ angular.module("notificationServices", [])
gold = Math.floor(absolute);
silver = Math.floor((absolute - gold) * 100);
if (gold && silver > 0) {
- return "" + gold + " " + silver + " ";
+ return "" + gold + " " + silver + " ";
} else if (gold > 0) {
- return "" + gold + " ";
+ return "" + gold + " ";
} else if (silver > 0) {
- return "" + silver + " ";
+ return "" + silver + " ";
}
};
@@ -45,11 +45,11 @@ angular.module("notificationServices", [])
coins: coins,
hp: function(val) {
// don't show notifications if user dead
- growl(" " + sign(val) + " " + round(val) + " HP", 'hp');
+ growl(" " + sign(val) + " " + round(val) + " " + window.env.t('hp'), 'hp');
},
exp: function(val) {
if (val < -50) return; // don't show when they level up (resetting their exp)
- growl(" " + sign(val) + " " + round(val) + " XP", 'xp');
+ growl(" " + sign(val) + " " + round(val) + " " + window.env.t('xp'), 'xp');
},
gp: function(val, bonus) {
growl(sign(val) + " " + coins(val - bonus), 'gp');
@@ -58,22 +58,19 @@ angular.module("notificationServices", [])
growl(val);
},
lvl: function(){
- growl(' Level Up!', 'lvl');
- },
- death: function(){
- growl(" Respawn!", "death");
+ growl(' ' + window.env.t('levelUp'), 'lvl');
},
error: function(error){
- growl(" " + error, "error");
+ growl(" " + error, "danger");
},
mp: function(val) {
- growl(" " + sign(val) + " " + round(val) + " MP", 'mp');
+ growl(" " + sign(val) + " " + round(val) + " " + window.env.t('mp'), 'mp');
},
crit: function(val) {
- growl(" Critical Hit! Bonus: " + Math.round(val) + "%", 'crit');
+ growl(" " + window.env.t('critBonus') + Math.round(val) + "%", 'crit');
},
drop: function(val) {
- growl(" " + val, 'drop');
+ growl(" " + val, 'drop');
}
};
}
diff --git a/public/js/static.js b/public/js/static.js
index d0b5683b76..84894ec5ab 100644
--- a/public/js/static.js
+++ b/public/js/static.js
@@ -5,3 +5,22 @@ window.habitrpgStatic = angular.module('habitrpgStatic', ['notificationServices'
.constant("STORAGE_USER_ID", 'habitrpg-user')
.constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings')
.constant("MOBILE_APP", false)
+
+habitrpgStatic.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({
+ maxWidth: '90%',
+ maxHeight: '80%',
+ transition: 'none',
+ scalePhotos:true
+ //maxHeight: '70%'
+ });
+ });
+}])
diff --git a/public/manifest.json b/public/manifest.json
index 2d616d2a19..85e39cd970 100644
--- a/public/manifest.json
+++ b/public/manifest.json
@@ -1,7 +1,7 @@
{
"app": {
"js": [
- "bower_components/jquery/jquery.js",
+ "bower_components/jquery/dist/jquery.min.js",
"bower_components/jquery.cookie/jquery.cookie.js",
"bower_components/bootstrap-growl/jquery.bootstrap-growl.js",
"bower_components/bootstrap-tour/build/js/bootstrap-tour.js",
@@ -10,7 +10,6 @@
"bower_components/marked/lib/marked.js",
"bower_components/angular-ui-router/release/angular-ui-router.js",
"bower_components/angular-resource/angular-resource.min.js",
- "bower_components/angular-ui/build/angular-ui.js",
"bower_components/angular-ui-utils/ui-utils.min.js",
"bower_components/angular-loading-bar/build/loading-bar.js",
"bower_components/Angular-At-Directive/src/at.js",
@@ -21,10 +20,11 @@
"bower_components/ngInfiniteScroll/ng-infinite-scroll.js",
"bower_components/select2/select2.js",
"bower_components/angular-ui-select2/src/select2.js",
+ "bower_components/JavaScriptButtons/dist/paypal-button.min.js",
- "bower_components/bootstrap/docs/assets/js/bootstrap.js",
"bower_components/angular-bootstrap/ui-bootstrap.js",
"bower_components/angular-bootstrap/ui-bootstrap-tpls.js",
+ "bower_components/bootstrap/dist/js/bootstrap.js",
"bower_components/jquery-ui/ui/minified/jquery.ui.core.min.js",
"bower_components/jquery-ui/ui/minified/jquery.ui.widget.min.js",
@@ -33,6 +33,8 @@
"bower_components/habitrpg-shared/dist/habitrpg-shared.js",
+ "js/env.js",
+
"js/app.js",
"js/services/sharedServices.js",
"js/services/authServices.js",
@@ -63,34 +65,33 @@
"js/controllers/hallCtrl.js"
],
"css": [
- "bower_components/bootstrap/docs/assets/css/bootstrap.css",
+ "bower_components/bootstrap/dist/css/bootstrap.css",
"app.css",
"bower_components/habitrpg-shared/dist/habitrpg-shared.css"
]
},
"static": {
"js": [
- "bower_components/jquery/jquery.js",
+ "bower_components/jquery/dist/jquery.min.js",
"bower_components/habitrpg-shared/dist/habitrpg-shared.js",
"bower_components/angular/angular.js",
-
- "bower_components/bootstrap/docs/assets/js/bootstrap.js",
-
"bower_components/angular-ui/build/angular-ui.js",
"bower_components/angular-bootstrap/ui-bootstrap.js",
"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/angular-loading-bar/build/loading-bar.js",
+ "js/env.js",
"js/static.js",
"js/services/notificationServices.js",
"bower_components/habitrpg-shared/script/userServices.js",
- "js/controllers/authCtrl.js"
+ "js/controllers/authCtrl.js",
+ "js/controllers/footerCtrl.js"
],
"css": [
- "bower_components/bootstrap/docs/assets/css/bootstrap.css",
- "bower_components/bootstrap/docs/assets/css/bootstrap-responsive.css",
- "bower_components/bootstrap/docs/assets/css/docs.css",
-
+ "bower_components/bootstrap/dist/css/bootstrap.css",
+ "bower_components/jquery-colorbox/example1/colorbox.css",
"static.css"
]
}
diff --git a/public/marketing/android_iphone.png b/public/marketing/android_iphone.png
new file mode 100644
index 0000000000..3256b4a668
Binary files /dev/null and b/public/marketing/android_iphone.png differ
diff --git a/public/marketing/animals.png b/public/marketing/animals.png
new file mode 100644
index 0000000000..ecae556c6e
Binary files /dev/null and b/public/marketing/animals.png differ
diff --git a/public/marketing/challenge.png b/public/marketing/challenge.png
new file mode 100644
index 0000000000..1b83b16b0f
Binary files /dev/null and b/public/marketing/challenge.png differ
diff --git a/public/marketing/drops.png b/public/marketing/drops.png
new file mode 100644
index 0000000000..2324e26eac
Binary files /dev/null and b/public/marketing/drops.png differ
diff --git a/public/marketing/education.png b/public/marketing/education.png
new file mode 100644
index 0000000000..b8c330419f
Binary files /dev/null and b/public/marketing/education.png differ
diff --git a/public/marketing/gear.png b/public/marketing/gear.png
new file mode 100644
index 0000000000..ef6bad6beb
Binary files /dev/null and b/public/marketing/gear.png differ
diff --git a/public/marketing/guild.png b/public/marketing/guild.png
new file mode 100644
index 0000000000..bbd66b3020
Binary files /dev/null and b/public/marketing/guild.png differ
diff --git a/public/marketing/guild_small.png b/public/marketing/guild_small.png
new file mode 100644
index 0000000000..28ab8fbea3
Binary files /dev/null and b/public/marketing/guild_small.png differ
diff --git a/public/marketing/integration.png b/public/marketing/integration.png
new file mode 100644
index 0000000000..290c5dc302
Binary files /dev/null and b/public/marketing/integration.png differ
diff --git a/public/marketing/lefnire.png b/public/marketing/lefnire.png
new file mode 100644
index 0000000000..657dc1eaf5
Binary files /dev/null and b/public/marketing/lefnire.png differ
diff --git a/public/marketing/social_competitve.png b/public/marketing/social_competitve.png
new file mode 100644
index 0000000000..3243a9d762
Binary files /dev/null and b/public/marketing/social_competitve.png differ
diff --git a/public/marketing/wellness.png b/public/marketing/wellness.png
new file mode 100644
index 0000000000..c91d295fc2
Binary files /dev/null and b/public/marketing/wellness.png differ
diff --git a/src/controllers/auth.js b/src/controllers/auth.js
index ab141c268e..c4d1541eaa 100644
--- a/src/controllers/auth.js
+++ b/src/controllers/auth.js
@@ -8,6 +8,7 @@ var async = require('async');
var utils = require('../utils');
var nconf = require('nconf');
var User = require('../models/user').model;
+var ga = require('./../utils').ga;
var api = module.exports;
@@ -39,7 +40,7 @@ api.authWithSession = function(req, res, next) { //[todo] there is probably a mo
if (!(req.session && req.session.userId))
return res.json(401, NO_SESSION_FOUND);
User.findOne({_id: uid}, function(err, user) {
- if (err) return res.json(500, {err: err});
+ if (err) return next(err);
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
res.locals.user = user;
next();
@@ -88,6 +89,7 @@ api.registerUser = function(req, res, next) {
};
user = new User(newUser);
user.save(cb);
+ ga.event('register', 'Local').send()
}
], function(err, saved) {
if (err) {
@@ -107,15 +109,15 @@ api.loginLocal = function(req, res, next) {
var password = req.body.password;
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
User.findOne({'auth.local.username': username}, function(err, user){
- if (err) return res.json(500,{err:err});
- if (!user) return res.json(401, {err:"Username '" + username + "' not found. Usernames are case-sensitive, click 'Forgot Password' if you can't remember the capitalization."});
+ if (err) return next(err);
+ if (!user) return res.json(401, {err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
// We needed the whole user object first so we can get his salt to encrypt password comparison
User.findOne({
'auth.local.username': username,
'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)
}, function(err, user){
- if (err) return res.json(500,{err:err});
- if (!user) return res.json(401,{err:'Incorrect password'});
+ if (err) return next(err);
+ if (!user) return res.json(401,{err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
res.json({id: user._id,token: user.apiToken});
});
});
@@ -164,7 +166,7 @@ api.resetPassword = function(req, res, next){
hashed_password = utils.encryptPassword(newPassword, salt);
User.findOne({'auth.local.email':email}, function(err, user){
- if (err) return res.json(500,{err:err});
+ if (err) return next(err);
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
user.auth.local.salt = salt;
user.auth.local.hashed_password = hashed_password;
@@ -187,18 +189,18 @@ api.changePassword = function(req, res, next) {
confirmNewPassword = req.body.confirmNewPassword;
if (newPassword != confirmNewPassword)
- return res.json(500, {err: "Password & Confirm don't match"});
+ return res.json(401, {err: "Password & Confirm don't match"});
var salt = user.auth.local.salt,
hashed_old_password = utils.encryptPassword(oldPassword, salt),
hashed_new_password = utils.encryptPassword(newPassword, salt);
if (hashed_old_password !== user.auth.local.hashed_password)
- return res.json(500, {err:"Old password doesn't match"});
+ return res.json(401, {err:"Old password doesn't match"});
user.auth.local.hashed_password = hashed_new_password;
user.save(function(err, saved){
- if (err) res.json(500,{err:err});
+ if (err) next(err);
res.send(200);
})
}
@@ -250,8 +252,7 @@ api.setupPassport = function(router) {
}
});
user.save(cb);
-
-
+ ga.event('register', 'Facebook').send()
}
], function(err, saved){
if (err) return res.redirect('/static/front?err=' + err);
diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js
index c0be3cc8e6..81f4e4e2c7 100644
--- a/src/controllers/challenges.js
+++ b/src/controllers/challenges.js
@@ -7,6 +7,7 @@ var shared = require('habitrpg-shared');
var User = require('./../models/user').model;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
+var logging = require('./../logging');
var csv = require('express-csv');
var api = module.exports;
@@ -214,7 +215,7 @@ api.update = function(req, res){
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
if (before.isOutdated(req.body)) {
User.find({_id: {$in: saved.members}}, function(err, users){
- console.log('Challenge updated, sync to subscribers');
+ logging.info('Challenge updated, sync to subscribers');
if (err) throw err;
_.each(users, function(user){
saved.syncToUser(user);
diff --git a/src/controllers/groups.js b/src/controllers/groups.js
index cfdaa2d179..4bdde2b83a 100644
--- a/src/controllers/groups.js
+++ b/src/controllers/groups.js
@@ -249,6 +249,14 @@ api.deleteChatMessage = function(req, res){
});
}
+api.seenMessage = function(req,res,next){
+ // Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save
+ var update = {$set:{}};
+ update['$set']['newMessages.'+req.params.gid+'.value'] = false;
+ User.update({_id:req.headers['x-api-user'], apiToken:req.headers['x-api-key']},update).exec();
+ res.send(200);
+}
+
api.likeChatMessage = function(req, res, next) {
var user = res.locals.user;
var group = res.locals.group;
diff --git a/src/controllers/hall.js b/src/controllers/hall.js
index 068efdfc3e..0edbd9e867 100644
--- a/src/controllers/hall.js
+++ b/src/controllers/hall.js
@@ -13,7 +13,7 @@ api.ensureAdmin = function(req, res, next) {
}
api.getHeroes = function(req,res,next) {
- User.find({'contributor.level':{$ne:null}})// {$exists:true} causes terrible performance http://goo.gl/GCxzC9
+ User.find({'contributor.level':{$gt:0}})
.select('contributor backer balance profile.name')
.sort('-contributor.level')
.exec(function(err, users){
diff --git a/src/controllers/user.js b/src/controllers/user.js
index f0f1f72cbe..b8979639c4 100644
--- a/src/controllers/user.js
+++ b/src/controllers/user.js
@@ -10,8 +10,10 @@ var validator = require('validator');
var check = validator.check;
var sanitize = validator.sanitize;
var User = require('./../models/user').model;
+var ga = require('./../utils').ga;
var Group = require('./../models/group').model;
var Challenge = require('./../models/challenge').model;
+var logging = require('./../logging');
var acceptablePUTPaths;
var api = module.exports;
@@ -33,7 +35,7 @@ api.getContent = function(req, res, next) {
---------------
*/
-findTask = function(req, res) {
+findTask = function(req, res, next) {
return task = res.locals.user.tasks[req.params.id];
};
@@ -80,17 +82,32 @@ api.score = function(req, res, next) {
var delta = user.ops.score({params:{id:task.id, direction:direction}});
user.save(function(err,saved){
- if (err) return res.json(500, {err: err});
+ if (err) return next(err);
// TODO this should be return {_v,task,stats,_tmp}, instead of merging everything togther at top-level response
// However, this is the most commonly used API route, and changing it will mess with all 3rd party consumers. Bad idea :(
res.json(200, _.extend({
delta: delta,
_tmp: user._tmp
}, saved.toJSON().stats));
- });
- // if it's a challenge task, sync the score
- user.syncScoreToChallenge(task, delta);
+ // 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
+ if (!task.challenge || !task.challenge.id || task.challenge.broken) return;
+ if (task.type == 'reward') return; // we don't want to update the reward GP cost
+ Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal){
+ if (err) return next(err);
+ if (!chal) {
+ task.challenge.broken = 'CHALLENGE_DELETED';
+ return user.save();
+ }
+ var t = chal.tasks[task.id];
+ if (!t) return chal.syncToUser(user); // this task was removed from the challenge, notify user
+ t.value += delta;
+ if (t.type == 'habit' || t.type == 'daily')
+ t.history.push({value: t.value, date: +new Date});
+ chal.save();
+ });
+ });
};
/**
@@ -192,7 +209,7 @@ api.update = function(req, res, next) {
});
user.save(function(err) {
if (!_.isEmpty(errors)) return res.json(401, {err: errors});
- if (err) return res.json(500, {err: err});
+ if (err) return next(err);
res.json(200, user);
});
};
@@ -232,9 +249,9 @@ api.cron = function(req, res, next) {
// api.reroll // Shared.ops
// api.reset // Shared.ops
-api['delete'] = function(req, res) {
+api['delete'] = function(req, res, next) {
res.locals.user.remove(function(err){
- if (err) return res.json(500,{err:err});
+ if (err) return next(err);
res.send(200);
})
}
@@ -253,11 +270,11 @@ api['delete'] = function(req, res) {
------------------------------------------------------------------------
*/
-api.addTenGems = function(req, res) {
+api.addTenGems = function(req, res, next) {
var user = res.locals.user;
user.balance += 2.5;
user.save(function(err){
- if (err) return res.json(500,{err:err});
+ if (err) return next(err);
res.send(204);
})
}
@@ -267,7 +284,7 @@ api.addTenGems = function(req, res) {
/*
Setup Stripe response when posting payment
*/
-api.buyGems = function(req, res) {
+api.buyGems = function(req, res, next) {
var api_key = nconf.get('STRIPE_API_KEY');
var stripe = require("stripe")(api_key);
var token = req.body.id;
@@ -300,8 +317,12 @@ api.buyGems = function(req, res) {
dateUpdated: new Date,
gemsBought: 0
};
+ ga.event('subscribe', 'Stripe').send()
+ ga.transaction(response.id, 5).item(5, 1, "stripe-subscription", "Subscription > Stripe").send()
} else {
user.balance += 5;
+ ga.event('checkout', 'Stripe').send()
+ ga.transaction(response.id, 5).item(5, 1, "stripe-checkout", "Gems > Stripe").send()
}
user.save(cb);
}
@@ -311,7 +332,7 @@ api.buyGems = function(req, res) {
});
};
-api.cancelSubscription = function(req, res) {
+api.cancelSubscription = function(req, res, next) {
var api_key = nconf.get('STRIPE_API_KEY');
var stripe = require("stripe")(api_key);
var user = res.locals.user;
@@ -330,6 +351,7 @@ api.cancelSubscription = function(req, res) {
], function(err, saved){
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
res.send(200, saved);
+ ga.event('unsubscribe', 'Stripe').send()
});
}
@@ -349,7 +371,9 @@ api.buyGemsPaypalIPN = function(req, res, next) {
user.balance += 5;
//user.purchased.ads = true;
user.save();
- console.log('PayPal transaction completed and user updated');
+ logging.info('PayPal transaction completed and user updated');
+ ga.event('checkout', 'PayPal').send()
+ ga.transaction(req.body.txn_id, 5).item(5, 1, "paypal-checkout", "Gems > PayPal").send()
});
}
});
@@ -369,7 +393,7 @@ api.buyGemsPaypalIPN = function(req, res, next) {
Spells
------------------------------------------------------------------------
*/
-api.cast = function(req, res) {
+api.cast = function(req, res, next) {
var user = res.locals.user;
var targetType = req.query.targetType;
var targetId = req.query.targetId;
@@ -380,7 +404,7 @@ api.cast = function(req, res) {
var done = function(){
var err = arguments[0];
var saved = _.size(arguments == 3) ? arguments[2] : arguments[1];
- if (err) return res.json(500, {err:err});
+ if (err) return next(err);
res.json(saved);
}
@@ -400,7 +424,7 @@ api.cast = function(req, res) {
case 'user':
async.waterfall([
function(cb){
- Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members', 'profile.name stats achievements').exec(cb);
+ Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members', 'profile.name stats achievements items.special').exec(cb);
},
function(group, cb) {
// Solo player? let's just create a faux group for simpler code
@@ -444,15 +468,15 @@ _.each(shared.wrap({}).ops, function(op,k){
res.locals.user.ops[k](req,function(err, response){
// If we want to send something other than 500, pass err as {code: 200, message: "Not enough GP"}
if (err) {
- if (!err.code) return res.json(500,{err:err});
+ if (!err.code) return next(err);
if (err.code >= 400) return res.json(err.code,{err:err.message});
// In the case of 200s, they're friendly alert messages like "You're pet has hatched!" - still send the op
}
res.locals.user.save(function(err){
- if (err) return res.json(500,{err:err});
+ if (err) return next(err);
res.json(200,response);
})
- })
+ }, ga);
}
}
})
@@ -472,28 +496,33 @@ api.batchUpdate = function(req, res, next) {
var oldSend = res.send;
var oldJson = res.json;
- var callOp = function(_req, cb) {
- res.send = res.json = function(code, data) {
- if (_.isNumber(code) && code >= 500)
- return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
- return cb();
- };
- api[_req.op](_req, res);
- };
+ // Stash user.save, we'll queue the save op till the end (so we don't overload the server)
+ var oldSave = user.save;
+ user.save = function(cb){cb(null,user)}
- res.locals.ops = [];
// Setup the array of functions we're going to call in parallel with async
- var ops = _.transform(req.body, function(result, _req) {
- if (!_.isEmpty(_req)) {
- result.push(function(cb) {
- res.locals.ops.push(_req);
- callOp(_req, cb);
- });
- }
+ res.locals.ops = [];
+ var ops = _.transform(req.body, function(m,_req){
+ if (_.isEmpty(_req)) return;
+ m.push(function() {
+ var cb = arguments[arguments.length-1];
+ res.locals.ops.push(_req);
+ res.send = res.json = function(code, data) {
+ if (_.isNumber(code) && code >= 500)
+ return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
+ return cb();
+ };
+ api[_req.op](_req, res, cb);
+ });
+ })
+ // Finally, save user at the end
+ .concat(function(){
+ user.save = oldSave;
+ user.save(arguments[arguments.length-1]);
});
// call all the operations, then return the user object to the requester
- async.series(ops, function(err) {
+ async.waterfall(ops, function(err,user) {
res.json = oldJson;
res.send = oldSend;
if (err) return next(err);
@@ -518,4 +547,4 @@ api.batchUpdate = function(req, res, next) {
res.json(200, {_v: response._v});
}
});
-};
\ No newline at end of file
+};
diff --git a/src/logging.js b/src/logging.js
new file mode 100644
index 0000000000..f8e56da7cf
--- /dev/null
+++ b/src/logging.js
@@ -0,0 +1,49 @@
+var nconf = require('nconf');
+var winston = require('winston');
+require('winston-mail').Mail;
+require('winston-newrelic');
+
+var logger;
+
+if (logger == null) {
+ logger = new (winston.Logger)({});
+ if (nconf.get('NODE_ENV') == 'production') {
+ logger.add(winston.transports.newrelic, {});
+ logger.add(winston.transports.Mail, {
+ to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'),
+ from: "HabitRPG <" + nconf.get('SMTP_USER') + ">",
+ subject: "HabitRPG Error",
+ host: nconf.get('SMTP_HOST'),
+ port: nconf.get('SMTP_PORT'),
+ tls: nconf.get('SMTP_TLS'),
+ username: nconf.get('SMTP_USER'),
+ password: nconf.get('SMTP_PASS'),
+ level: 'error'
+ });
+ } else {
+ logger.add(winston.transports.Console, {colorize:true});
+ logger.add(winston.transports.File, {filename: 'habitrpg.log'});
+ }
+}
+
+// A custom log function that wraps Winston. Makes it easy to instrument code
+// and still possible to replace Winston in the future.
+module.exports.log = function(/* variable args */) {
+ if (logger)
+ logger.log.apply(logger, arguments);
+};
+
+module.exports.info = function(/* variable args */) {
+ if (logger)
+ logger.info.apply(logger, arguments);
+};
+
+module.exports.warn = function(/* variable args */) {
+ if (logger)
+ logger.warn.apply(logger, arguments);
+};
+
+module.exports.error = function(/* variable args */) {
+ if (logger)
+ logger.error.apply(logger, arguments);
+};
diff --git a/src/middleware.js b/src/middleware.js
index 19ada70aaf..4894b586d1 100644
--- a/src/middleware.js
+++ b/src/middleware.js
@@ -4,24 +4,57 @@ var fs = require('fs');
var path = require('path');
var User = require('./models/user').model
var limiter = require('connect-ratelimit');
+var logging = require('./logging');
+var domainMiddleware = require('domain-middleware');
+var cluster = require('cluster');
module.exports.apiThrottle = function(app) {
+ if (nconf.get('NODE_ENV') !== 'production') return;
app.use(limiter({
end:false,
catagories:{
normal: {
// 2 req/s, but split as minutes
- totalRequests: 120,
+ totalRequests: 80,
every: 60000
}
}
})).use(function(req,res,next){
- //console.log(res.ratelimit);
+ //logging.info(res.ratelimit);
if (res.ratelimit.exceeded) return res.json(429,{err:'Rate limit exceeded'});
next();
});
}
+module.exports.domainMiddleware = function(server,mongoose) {
+ return domainMiddleware({
+ server: {
+ close:function(){
+ server.close();
+ mongoose.connection.close();
+ }
+ },
+ killTimeout: 10000
+ });
+}
+
+module.exports.errorHandler = function(err, req, res, next) {
+ //res.locals.domain.emit('error', err);
+ // when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER)
+ var stack = (err.stack ? err.stack : err.message ? err.message : err) +
+ "\n ----------------------------\n" +
+ "\n\noriginalUrl: " + req.originalUrl +
+ "\n\nauth: " + req.headers['x-api-user'] + ' | ' + req.headers['x-api-key'] +
+ "\n\nheaders: " + JSON.stringify(req.headers) +
+ "\n\nbody: " + JSON.stringify(req.body) +
+ (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
+ logging.error(stack);
+ var message = err.message ? err.message : err;
+ message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
+ res.json(500,{err:message}); //res.end(err.message);
+}
+
+
module.exports.forceSSL = function(req, res, next){
var baseUrl = nconf.get("BASE_URL");
// Note x-forwarded-proto is used by Heroku & nginx, you'll have to do something different if you're not using those
@@ -89,22 +122,21 @@ var getManifestFiles = function(page){
if(!files) throw new Error("Page not found!");
- var css = '';
-
- _.each(files.css, function(file){
- css += '';
- });
+ var code = '';
if(nconf.get('NODE_ENV') === 'production'){
- return css + '';
+ code += '';
+ code += '';
}else{
- var results = css;
- _.each(files.js, function(file){
- results += '';
+ _.each(files.css, function(file){
+ code += '';
+ });
+ _.each(files.js, function(file){
+ code += '';
});
- return results;
}
-
+
+ return code;
}
// Translations
@@ -112,12 +144,12 @@ var getManifestFiles = function(page){
var translations = {};
var loadTranslations = function(locale){
- var files = require(path.join(__dirname, "/../node_modules/habitrpg-shared/locales/", locale, 'app.json')).files;
+ var files = fs.readdirSync(path.join(__dirname, "/../node_modules/habitrpg-shared/locales/", locale));
translations[locale] = {};
_.each(files, function(file){
_.merge(translations[locale], require(path.join(__dirname, "/../node_modules/habitrpg-shared/locales/", locale, file)));
});
-}
+};
// First fetch english so we can merge with missing strings in other languages
loadTranslations('en');
@@ -164,7 +196,6 @@ var getUserLanguage = function(req, callback){
var acceptable = _(req.acceptedLanguages).map(function(lang){
return lang.slice(0, 2);
}).uniq().value();
-
var matches = _.intersection(acceptable, langCodes);
return matches.length > 0 ? matches[0] : 'en';
};
@@ -176,44 +207,52 @@ var getUserLanguage = function(req, callback){
return callback(null, _.find(avalaibleLanguages, {code: user.preferences.language}));
}else{
var langCode = getFromBrowser();
- if(user && translations[langCode]){
- user.preferences.language = langCode;
- user.save(); //callback?
- }
+ // Because english is usually always avalaible as an acceptable language for the browser,
+ // if the user visit the page when his own language is not avalaible yet
+ // he'll have english set in his preferences, which is not good.
+ //if(user && translations[langCode]){
+ //user.preferences.language = langCode;
+ //user.save(); //callback?
+ //}
return callback(null, _.find(avalaibleLanguages, {code: langCode}))
}
});
}else{
- return callback(null, _.find(avalaibleLanguages, {code: getFromBrowser()}));
+ return callback(null, _.find(avalaibleLanguages, {code: getFromBrowser()}));
}
}
module.exports.locals = function(req, res, next) {
getUserLanguage(req, function(err, language){
- if(err) return res.json(500, {err: err});
+ if(err) return res.json(500, {err: err});
+
+ var isStaticPage = req.url.split('/')[1] === 'static'; // If url contains '/static/'
+
+ // Load moment.js language file only when not on static pages
+ language.momentLang = ((!isStaticPage && momentLangs[language.code])|| undefined);
- language.momentLang = (momentLangs[language.code] || undefined);
-
res.locals.habitrpg = {
NODE_ENV: nconf.get('NODE_ENV'),
BASE_URL: nconf.get('BASE_URL'),
PAYPAL_MERCHANT: nconf.get('PAYPAL_MERCHANT'),
+ GA_ID: nconf.get("GA_ID"),
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
STRIPE_PUB_KEY: nconf.get('STRIPE_PUB_KEY'),
getManifestFiles: getManifestFiles,
getBuildUrl: getBuildUrl,
avalaibleLanguages: avalaibleLanguages,
language: language,
+ isStaticPage: isStaticPage,
translations: translations[language.code],
t: function(stringName, vars){
var string = translations[language.code][stringName];
if(!string) return _.template(translations[language.code].stringNotFound, {string: stringName});
- return vars === undefined ? string : _.template(string, vars);
+ return vars === undefined ? string : _.template(string, vars);
},
siteVersion: siteVersion
}
- next();
+ next();
});
-}
\ No newline at end of file
+}
diff --git a/src/models/challenge.js b/src/models/challenge.js
index 2656bebe1b..a01cea9575 100644
--- a/src/models/challenge.js
+++ b/src/models/challenge.js
@@ -3,7 +3,6 @@ var Schema = mongoose.Schema;
var shared = require('habitrpg-shared');
var _ = require('lodash');
var TaskSchemas = require('./task');
-var Group = require('./group').model;
var ChallengeSchema = new Schema({
_id: {type: String, 'default': shared.uuid},
diff --git a/src/models/group.js b/src/models/group.js
index fa901dad8f..0ecc16221e 100644
--- a/src/models/group.js
+++ b/src/models/group.js
@@ -3,7 +3,6 @@ var Schema = mongoose.Schema;
var shared = require('habitrpg-shared');
var _ = require('lodash');
var async = require('async');
-var User = require('./user').model;
var GroupSchema = new Schema({
_id: {type: String, 'default': shared.uuid},
@@ -107,6 +106,17 @@ GroupSchema.methods.sendChat = function(message, user){
}
group.chat.unshift(message);
group.chat.splice(200);
+
+ // Kick off chat notifications in the background.
+ var lastSeenUpdate = {$set:{}, $inc:{_v:1}};
+ lastSeenUpdate['$set']['newMessages.'+group._id] = {name:group.name,value:true};
+ if (group._id == 'habitrpg') {
+ // TODO For Tavern, only notify them if their name was mentioned
+ // var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names?
+ // User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec();
+ } else {
+ mongoose.model('User').update({_id:{$in:group.members, $ne: user ? user._id : ''}},lastSeenUpdate,{multi:true}).exec();
+ }
}
var cleanQuestProgress = function(merge){
@@ -161,8 +171,6 @@ GroupSchema.methods.finishQuest = function(quest, cb) {
})
var members = _.keys(group.quest.members);
group.quest = {};group.markModified('quest');
- // FIXME this is TERRIBLE practice. Looks like there are circular dependencies in the models, such that `var User` at
- // this point is undefined. So we get around that by loading from mongoose only once we get to this point
mongoose.models.User.update({_id:{$in:members}}, updates, {multi:true}, cb);
}
diff --git a/src/models/user.js b/src/models/user.js
index b442980d0b..9c1c5ba0cc 100644
--- a/src/models/user.js
+++ b/src/models/user.js
@@ -42,7 +42,8 @@ var UserSchema = new Schema({
rebirths: Number,
rebirthLevel: Number,
perfect: Number,
- habitBirthday: Boolean
+ habitBirthday: Boolean,
+ valentine: Number
},
auth: {
facebook: Schema.Types.Mixed,
@@ -68,7 +69,8 @@ var UserSchema = new Schema({
level: Number, // 1-7, see https://trello.com/c/wkFzONhE/277-contributor-gear
admin: Boolean,
text: String, // Artisan, Friend, Blacksmith, etc
- contributions: String // a markdown textarea to list their contributions + links
+ contributions: String, // a markdown textarea to list their contributions + links
+ critical: String
},
balance: {type: Number, 'default':0},
@@ -135,10 +137,12 @@ var UserSchema = new Schema({
},
special:{
- snowball: {type: Number, 'default': 0}
+ snowball: {type: Number, 'default': 0},
+ valentine: Number,
+ valentineReceived: Array // array of strings, by sender name
},
- // -------------- Animals -------------------
+ // -------------- Animals -------------------
// Complex bit here. The result looks like:
// pets: {
// 'Wolf-Desert': 0, // 0 means does not own
@@ -149,6 +153,8 @@ var UserSchema = new Schema({
_.defaults(
// First transform to a 1D eggs/potions mapping
_.transform(shared.content.pets, function(m,v,k){ m[k] = Number; }),
+ // Then add quest pets
+ _.transform(shared.content.questPets, function(m,v,k){ m[k] = Number; }),
// Then add additional pets (backer, contributor)
_.transform(shared.content.specialPets, function(m,v,k){ m[k] = Number; })
),
@@ -180,6 +186,8 @@ var UserSchema = new Schema({
mounts: _.defaults(
// First transform to a 1D eggs/potions mapping
_.transform(shared.content.pets, function(m,v,k){ m[k] = Boolean; }),
+ // Then add quest pets
+ _.transform(shared.content.questPets, function(m,v,k){ m[k] = Boolean; }),
// Then add additional pets (backer, contributor)
{
'LionCub-Ethereal': Boolean,
@@ -202,9 +210,11 @@ var UserSchema = new Schema({
lastCron: {type: Date, 'default': Date.now},
+ // {GROUP_ID: Boolean}, represents whether they have unseen chat messages
+ newMessages: {type: Schema.Types.Mixed, 'default': {}},
+
party: {
// id // FIXME can we use a populated doc instead of fetching party separate from user?
- lastMessageSeen: String,
order: {type:String, 'default':'level'},
quest: {
key: String,
@@ -240,7 +250,8 @@ var UserSchema = new Schema({
disableClasses: {type: Boolean, 'default': false},
newTaskEdit: {type: Boolean, 'default': false},
tagsCollapsed: {type: Boolean, 'default': false},
- advancedCollapsed: {type: Boolean, 'default': false}
+ advancedCollapsed: {type: Boolean, 'default': false},
+ toolbarCollapsed: {type:Boolean, 'default':false}
},
profile: {
blurb: String,
@@ -362,22 +373,6 @@ UserSchema.pre('save', function(next) {
next();
});
-UserSchema.methods.syncScoreToChallenge = function(task, delta){
- if (!task.challenge || !task.challenge.id || task.challenge.broken) return;
- if (task.type == 'reward') return; // we don't want to update the reward GP cost
- var self = this;
- Challenge.findById(task.challenge.id, function(err, chal){
- if (err) throw err;
- var t = chal.tasks[task.id];
- if (!t) return chal.syncToUser(self); // this task was removed from the challenge, notify user
- t.value += delta;
- if (t.type == 'habit' || t.type == 'daily') {
- t.history.push({value: t.value, date: +new Date});
- }
- chal.save();
- });
-}
-
UserSchema.methods.unlink = function(options, cb) {
var cid = options.cid, keep = options.keep, tid = options.tid;
var self = this;
diff --git a/src/routes/apiv1.js b/src/routes/apiv1.js
index 53207631d2..ce09e8a24a 100644
--- a/src/routes/apiv1.js
+++ b/src/routes/apiv1.js
@@ -6,6 +6,7 @@ var icalendar = require('icalendar');
var api = require('./../controllers/user');
var auth = require('./../controllers/auth');
var middleware = require('../middleware');
+var logging = require('./../logging');
/* ---------- Deprecated API ------------*/
@@ -78,7 +79,7 @@ var batchUpdate = function(req, res, next) {
req.body = action.data;
res.send = res.json = function(code, data) {
if (_.isNumber(code) && code >= 400) {
- console.error({
+ logging.error({
code: code,
data: data
});
@@ -168,4 +169,4 @@ router.get('*', deprecated);
router.post('*', deprecated);
router.put('*', deprecated);
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee
index 513fb7e312..db5a0010f0 100644
--- a/src/routes/apiv2.coffee
+++ b/src/routes/apiv2.coffee
@@ -488,6 +488,17 @@ module.exports = (swagger, v2) ->
middleware: [auth.auth, groups.attachGroup]
action: groups.postChat
+ # placing before route below, so that if !=='seen' it goes to next()
+ "/groups/{gid}/chat/seen":
+ spec:
+ method: 'POST'
+ description: "Flag chat messages for a particular group as seen"
+ parameters: [
+ path 'gid','Group id','string'
+ ]
+ middleware: []
+ action: groups.seenMessage
+
"/groups/{gid}/chat/{messageId}":
spec:
method: 'DELETE'
diff --git a/src/routes/pages.js b/src/routes/pages.js
index a5ef570e2c..20902b09a3 100644
--- a/src/routes/pages.js
+++ b/src/routes/pages.js
@@ -18,9 +18,7 @@ router.get('/', middleware.locals, function(req, res) {
// -------- Marketing --------
router.get('/static/front', middleware.locals, function(req, res) {
- var env = res.locals.habitrpg;
- env.isFrontPage = true;
- res.render('static/front', {env: env});
+ res.render('static/front', {env: res.locals.habitrpg});
});
router.get('/static/privacy', middleware.locals, function(req, res) {
@@ -39,6 +37,10 @@ router.get('/static/about', middleware.locals, function(req, res) {
res.render('static/about', {env: res.locals.habitrpg});
});
+router.get('/static/videos', middleware.locals, function(req, res) {
+ res.render('static/videos', {env: res.locals.habitrpg});
+});
+
router.get('/static/contact', middleware.locals, function(req, res) {
res.render('static/contact', {env: res.locals.habitrpg});
});
diff --git a/src/seed.js b/src/seed.js
index becd3cf94b..111dfee1df 100644
--- a/src/seed.js
+++ b/src/seed.js
@@ -1,6 +1,7 @@
require('coffee-script') // for habitrpg-shared
var nconf = require('nconf');
var utils = require('./utils');
+var logging = require('./logging');
utils.setupConfig();
var async = require('async');
var mongoose = require('mongoose');
@@ -15,7 +16,7 @@ async.waterfall([
Group.findById('habitrpg', cb);
},
function(tavern, cb){
- console.log({tavern:tavern,cb:cb});
+ logging.info({tavern:tavern,cb:cb});
if (!tavern) {
tavern = new Group({
_id: 'habitrpg',
@@ -31,6 +32,6 @@ async.waterfall([
}
],function(err){
if (err) throw err;
- console.log("Done initializing database");
+ logging.info("Done initializing database");
mongoose.disconnect();
})
diff --git a/src/server.js b/src/server.js
index 8d95852ad0..bef7642207 100644
--- a/src/server.js
+++ b/src/server.js
@@ -4,19 +4,19 @@ var _ = require('lodash');
var nconf = require('nconf');
var utils = require('./utils');
utils.setupConfig();
-
+var logging = require('./logging');
var isProd = nconf.get('NODE_ENV') === 'production';
var isDev = nconf.get('NODE_ENV') === 'development';
if (cluster.isMaster && (isDev || isProd)) {
// Fork workers.
- _.times(require('os').cpus().length, function(){
+ _.times(_.min([require('os').cpus().length,2]), function(){
cluster.fork();
- })
+ });
- cluster.on('exit', function(worker, code, signal) {
+ cluster.on('disconnect', function(worker, code, signal) {
var w = cluster.fork(); // replace the dead worker
- console.error('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid);
+ logging.info('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid);
});
} else {
@@ -24,7 +24,6 @@ if (cluster.isMaster && (isDev || isProd)) {
var express = require("express");
var http = require("http");
var path = require("path");
- var domainMiddleware = require('domain-middleware');
var swagger = require("swagger-node-express");
var middleware = require('./middleware');
@@ -35,14 +34,18 @@ if (cluster.isMaster && (isDev || isProd)) {
// ------------ MongoDB Configuration ------------
mongoose = require('mongoose');
- require('./models/user'); //load up the user schema - TODO is this necessary?
- require('./models/group');
- require('./models/challenge');
- mongoose.connect(nconf.get('NODE_DB_URI'), {auto_reconnect:true}, function(err) {
- if (err) throw err;
- console.info('Connected with Mongoose');
+ var mongooseOptions = !isProd ? {} : {
+ replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
+ server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }
+ };
+ mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, function(err) {
+ if (err) throw err;
+ logging.info('Connected with Mongoose');
});
-
+ // load schemas & models
+ require('./models/challenge');
+ require('./models/group');
+ require('./models/user');
// ------------ Passport Configuration ------------
var passport = require('passport')
@@ -63,7 +66,6 @@ if (cluster.isMaster && (isDev || isProd)) {
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
@@ -88,14 +90,9 @@ if (cluster.isMaster && (isDev || isProd)) {
// ------------ Server Configuration ------------
- domainMiddleware({
- server: server,
- killTimeout: 3000
- }),
-
app.set("port", nconf.get('PORT'));
-
middleware.apiThrottle(app);
+ app.use(middleware.domainMiddleware(server,mongoose));
if (!isProd) app.use(express.logger("dev"));
app.use(express.compress());
app.set("views", __dirname + "/../views");
@@ -119,14 +116,11 @@ if (cluster.isMaster && (isDev || isProd)) {
app.use(app.router);
var maxAge = isProd ? 31536000000 : 0;
+ // Cache emojis without copying them to build, they are too many
app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
+ app.use('/bower_components/habitrpg-shared/img/emoji/unicode', express['static'](path.join(__dirname, "/../public/bower_components/habitrpg-shared/img/emoji/unicode"), { maxAge: maxAge }));
app.use(express['static'](path.join(__dirname, "/../public")));
- // development only
- //if ("development" === app.get("env")) {
- // app.use(express.errorHandler());
- //}
-
// Custom Directives
app.use(require('./routes/pages').middleware);
app.use(require('./routes/auth').middleware);
@@ -134,15 +128,13 @@ if (cluster.isMaster && (isDev || isProd)) {
app.use('/api/v2', v2);
app.use('/api/v1', require('./routes/apiv1').middleware);
app.use('/export', require('./routes/dataexport').middleware);
-
- app.use(utils.errorHandler);
-
require('./routes/apiv2.coffee')(swagger, v2);
+ app.use(middleware.errorHandler);
server.on('request', app);
server.listen(app.get("port"), function() {
- return console.log("Express server listening on port " + app.get("port"));
+ return logging.info("Express server listening on port " + app.get("port"));
});
module.exports = server;
-}
\ No newline at end of file
+}
diff --git a/src/utils.js b/src/utils.js
index eacb9e7c59..b5f51ccb91 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -3,6 +3,8 @@ var nconf = require('nconf');
var crypto = require('crypto');
var path = require("path");
+module.exports.ga = undefined; // set Google Analytics on nconf init
+
module.exports.sendEmail = function(mailData) {
var smtpTransport = nodemailer.createTransport("SMTP",{
service: nconf.get('SMTP_SERVICE'),
@@ -12,8 +14,9 @@ module.exports.sendEmail = function(mailData) {
}
});
smtpTransport.sendMail(mailData, function(error, response){
- if(error) console.log(error);
- else console.log("Message sent: " + response.message);
+ var logging = require('./logging');
+ if(error) logging.error(error);
+ else logging.info("Message sent: " + response.message);
smtpTransport.close(); // shut down the connection pool, no more messages
});
}
@@ -31,8 +34,6 @@ module.exports.makeSalt = function() {
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
}
-
-
/**
* Load nconf and define default configuration values if config.json or ENV vars are not found
*/
@@ -42,42 +43,10 @@ module.exports.setupConfig = function(){
//.file('defaults', path.join(path.resolve(__dirname, '../config.json.example')))
.file('user', path.join(path.resolve(__dirname, '../config.json')));
-// var agent;
-// if (process.env.NODE_ENV === 'development') {
-// // Follow these instructions for profiling / debugging leaks
-// // * https://developers.google.com/chrome-developer-tools/docs/heap-profiling
-// // * https://developers.google.com/chrome-developer-tools/docs/memory-analysis-101
-// agent = require('webkit-devtools-agent');
-// console.log("To debug memory leaks:" +
-// "\n\t(1) Run `kill -SIGUSR2 " + process.pid + "`" +
-// "\n\t(2) open http://c4milo.github.com/node-webkit-agent/21.0.1180.57/inspector.html?host=localhost:1337&page=0");
-// }
-
- if (nconf.get('NODE_ENV') === "development") {
+ if (nconf.get('NODE_ENV') === "development")
Error.stackTraceLimit = Infinity;
- }
- if (nconf.get('NODE_ENV') === 'production') require('newrelic');
-};
+ if (nconf.get('NODE_ENV') === 'production')
+ require('newrelic');
-
-module.exports.errorHandler = function(err, req, res, next) {
- // when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER)
- var stack = (err.stack ? err.stack : err.message ? err.message : err) +
- "\n ----------------------------\n" +
- "\n\noriginalUrl: " + req.originalUrl +
- "\n\nauth: " + req.headers['x-api-user'] + ' | ' + req.headers['x-api-key'] +
- "\n\nheaders: " + JSON.stringify(req.headers) +
- "\n\nbody: " + JSON.stringify(req.body) +
- (res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
- module.exports.sendEmail({
- from: "HabitRPG <" + nconf.get('SMTP_USER') + ">",
- to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'),
- subject: "HabitRPG Error",
- text: stack
- });
- console.error(stack);
- var message = err.message ? err.message : err;
- message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
- res.json(500,{err:message}); //res.end(err.message);
- process.exit(0);
-}
\ No newline at end of file
+ module.exports.ga = require('universal-analytics')(nconf.get('GA_ID'));
+};
\ No newline at end of file
diff --git a/test/api.mocha.js b/test/api.mocha.js
index 9b69fd461e..3f4f20d6f2 100644
--- a/test/api.mocha.js
+++ b/test/api.mocha.js
@@ -253,6 +253,33 @@ describe('API', function () {
});
});
+ it('Challenge deleted, breaks task link', function (done) {
+ var itThis = this;
+ request.del(baseURL + "/challenges/" + challenge._id).end(function (res) {
+ User.findById(user._id, function(err,user){
+ var len = user.dailys.length- 1,
+ daily = user.dailys[user.dailys.length-1];
+ expect(daily.challenge.broken).to.be('CHALLENGE_DELETED');
+
+ // Now let's handle if challenge was deleted, but didn't get to update all the users (an error)
+ var unset = {$unset:{}};
+ unset['$unset']['dailys.' + len + '.challenge.broken'] = 1;
+ User.findByIdAndUpdate(user._id,unset,function(err,user){
+ expect(err).to.not.be.ok();
+ expect(user.dailys[len].challenge.broken).to.not.be.ok();
+ request.post(baseURL + "/user/tasks/" + daily.id + "/up").end(function (res) {
+ setTimeout(function(){
+ User.findById(user._id,function(err,user){
+ expect(user.dailys[len].challenge.broken).to.be('CHALLENGE_DELETED');
+ done()
+ })
+ }, 100); // we need to wait for challenge to update user, it's a background job for perf reasons
+ })
+ })
+ })
+ })
+ });
+
it('Admin creates a challenge', function (done) {
User.findByIdAndUpdate(_id, {$set:{'contributor.admin':true}}, function (err,_user) {
expect(err).to.not.be.ok();
diff --git a/views/index.jade b/views/index.jade
index 48fcae33aa..85f6c5d642 100644
--- a/views/index.jade
+++ b/views/index.jade
@@ -3,43 +3,34 @@ doctype 5
html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":applyingAction}', ui-keyup="{27:'castCancel()'}")
head
title=env.t('titleIndex')
-
// ?v=1 needed to force refresh
- link(rel='shortcut icon', href='#{env.getBuildUrl("favicon.ico")}?v=2')
+ 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)};
- style.
- [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- display: none;
- }
-
!= env.getManifestFiles("app")
//webfonts
link(href='//fonts.googleapis.com/css?family=Lato:300,400,700,400italic,700italic', rel='stylesheet', type='text/css')
- meta(name='viewport', content='width=device-width')
- meta(name='apple-mobile-web-app-capable', content='yes')
- body(ng-cloak)
- div(ng-controller='GroupsCtrl')
- include ./shared/modals/index
- include ./shared/header/header
- include ./shared/tasks/lists
- include ./main/index
- include ./options/index
-
- #notification-area(ng-controller='NotificationCtrl')
- #wrap
-
- //if they hide the header, we still need user-menu visible
- div(ng-if='user.preferences.hideHeader')
- include ./shared/header/menu
-
- .exp-chart(ng-show='charts.exp')
+ body(ng-cloak, ng-controller='GroupsCtrl')
+ include ./shared/header/menu
+ include ./shared/modals/index
+ include ./shared/header/header
+ include ./shared/tasks/lists
+ include ./main/index
+ include ./options/index
+ #notification-area(ng-controller='NotificationCtrl')
+ #wrap.container-fluid
+ .row
+ .col-md-12.exp-chart(ng-show='charts.exp')
#main(ui-view)
- include ./shared/footer
\ No newline at end of file
+ include ./shared/footer
\ No newline at end of file
diff --git a/views/main/filters.jade b/views/main/filters.jade
index 0a5de7e1db..e356e552e5 100644
--- a/views/main/filters.jade
+++ b/views/main/filters.jade
@@ -1,26 +1,29 @@
-.filters.tag-list(ng-controller='FiltersCtrl')
- .pull-left.filter-description
- =env.t('tags')
- |:
- ul.nav.nav-pills
- li
- a(rel='tooltip', title=env.t('editTags'), ng-click='saveOrEdit()')
- i(ng-class='{"icon-ok": _editing, "icon-pencil": !_editing}')
- li(bindonce='user.tags', ng-class='{active: user.filters[tag.id]}', ng-repeat='tag in user.tags', style='position: relative;')
- .input-append.option-group.tag-editing(ng-show='_editing')
- input.input.input-small.option-content.tag-editing-pill(type='text', ng-model='tag.name')
- span.add-on.tag-editing-pill
- a.pull-right(ng-click='user.ops.deleteTag({params:{id:tag.id}})')
- i.icon-trash
- a(ng-hide='_editing', ng-click='toggleFilter(tag)')
- i.icon-bullhorn(bo-if="tag.challenge")
- markdown(ng-model='tag.name',style='display: inline-block')
- li
- form.form-inline(ng-show='_editing', ng-submit='createTag(_newTag)')
- .input-append.tag-editing
- input.span2.tag-editing-pill(type='text', ng-model='_newTag', placeholder=env.t('newTag'))
- button.add-on.tag-editing-pill(type='submit')=env.t('add')
- //