diff --git a/.bowerrc b/.bowerrc index 9c9ea40600..20772d5d24 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,8 @@ { - "directory": "public/bower_components" + "directory": "public/bower_components", + "storage": { + "packages": ".bower-cache", + "registry": ".bower-registry" + }, + "tmp": ".bower-tmp" } \ No newline at end of file diff --git a/.gitignore b/.gitignore index a7d8e7d2e6..ef989eaea0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ npm-debug.log lib public/bower_components build +newrelic_agent.log src/*/*.map src/*/*/*.map diff --git a/.nodemonignore b/.nodemonignore index 216c52f8b0..f4e0b53926 100644 --- a/.nodemonignore +++ b/.nodemonignore @@ -6,3 +6,4 @@ Gruntfile.js CHANGELOG.md .idea* .git* +newrelic_agent.log diff --git a/config.json.example b/config.json.example index 5cf560aae6..634d22c7a7 100644 --- a/config.json.example +++ b/config.json.example @@ -13,5 +13,6 @@ "SMTP_SERVICE":"Gmail", "STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", "STRIPE_PUB_KEY":"22223333444455556666777788889999", - "PAYPAL_MERCHANT":"paypal-merchant@gmail.com" + "PAYPAL_MERCHANT":"paypal-merchant@gmail.com", + "NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY" } diff --git a/migrations/20140130_birthdayEnd.js b/migrations/20140130_birthdayEnd.js new file mode 100644 index 0000000000..a9a8eb93bc --- /dev/null +++ b/migrations/20140130_birthdayEnd.js @@ -0,0 +1 @@ +db.users.update({},{$set:{'achievements.habitBirthday':true}},{multi:1}) diff --git a/migrations/20140130_birthdayStart.js b/migrations/20140130_birthdayStart.js new file mode 100644 index 0000000000..f022168bcd --- /dev/null +++ b/migrations/20140130_birthdayStart.js @@ -0,0 +1,12 @@ +db.users.update({},{$set:{ + 'items.food.Cake_Skeleton':1, + 'items.food.Cake_Base':1, + 'items.food.Cake_CottonCandyBlue':1, + 'items.food.Cake_CottonCandyPink':1, + 'items.food.Cake_Shade':1, + 'items.food.Cake_White':1, + 'items.food.Cake_Golden':1, + 'items.food.Cake_Zombie':1, + 'items.food.Cake_Desert':1, + 'items.food.Cake_Red':1 +}},{multi:1}) diff --git a/newrelic.js b/newrelic.js new file mode 100644 index 0000000000..682f608e42 --- /dev/null +++ b/newrelic.js @@ -0,0 +1,25 @@ +/** + * New Relic agent configuration. + * + * See lib/config.defaults.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ +var nconf = require('nconf') +exports.config = { + /** + * Array of application names. + */ + app_name : ['HabitRPG'], + /** + * Your New Relic license key. + */ + license_key : nconf.get('NEW_RELIC_LICENSE_KEY'), + logging : { + /** + * Level at which to log. 'trace' is most useful to New Relic when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level : 'warning' + } +}; diff --git a/package.json b/package.json index 2ecac913bc..338485de34 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "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" + "passport-facebook": "~1.0.2", + "newrelic": "~1.3.0", + "connect-ratelimit": "0.0.6" }, "private": true, "subdomain": "habitrpg", diff --git a/src/controllers/auth.js b/src/controllers/auth.js index dd508039ad..ab141c268e 100644 --- a/src/controllers/auth.js +++ b/src/controllers/auth.js @@ -24,7 +24,7 @@ api.auth = function(req, res, next) { var token = req.headers['x-api-key']; if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID); User.findOne({_id: uid,apiToken: token}, 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.wasModified = req.query._v ? +user._v !== +req.query._v : true; diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 254d8e7b2f..53d0a6f040 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -147,16 +147,15 @@ api.create = function(req, res, next) { group.balance = 1; user.balance--; - user.save(function(err){ - if(err) return res.json(500,{err:err}); - group.save(function(err, saved){ - if (err) return res.json(500,{err:err}); - saved.populate('members', nameFields, function(err, populated){ - if (err) return res.json(500,{err:err}); - return res.json(populated); - }); - }); - }); + async.waterfall([ + function(cb){user.save(cb)}, + function(saved,ct,cb){group.save(cb)}, + function(saved,ct,cb){saved.populate('members',nameFields,cb)} + ],function(err,saved){ + if (err) return next(err); + res.json(saved); + }); + }else{ async.waterfall([ function(cb){ @@ -250,7 +249,7 @@ api.deleteChatMessage = function(req, res){ }); } -api.likeChatMessage = function(req, res) { +api.likeChatMessage = function(req, res, next) { var user = res.locals.user; var group = res.locals.group; var message = _.find(group.chat, {id: req.params.mid}); @@ -264,6 +263,7 @@ api.likeChatMessage = function(req, res) { } group.markModified('chat'); group.save(function(err,_saved){ + if (err) return next(err); return res.send(_saved.chat); }) } diff --git a/src/controllers/hall.js b/src/controllers/hall.js index 2498112785..068efdfc3e 100644 --- a/src/controllers/hall.js +++ b/src/controllers/hall.js @@ -25,7 +25,7 @@ api.getHeroes = function(req,res,next) { api.getPatrons = function(req,res,next){ var page = req.query.page || 0, perPage = 50; - User.find({'backer.tier':{$ne:null}}) + User.find({'backer.tier':{$gt:0}}) .select('contributor backer profile.name') .sort('-backer.tier') .skip(page*perPage) diff --git a/src/controllers/user.js b/src/controllers/user.js index 13fa2d00b4..bd167da816 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -166,6 +166,9 @@ acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function if (found) m[leaf]=true; return m; }, {}) +_.each('stats.gp'.split(' '), function(removePath){ + delete acceptablePUTPaths[removePath]; +}) /** * Update user @@ -186,8 +189,8 @@ api.update = function(req, res, next) { return true; }); user.save(function(err) { - if (!_.isEmpty(errors)) return res.json(500, {err: errors}); - if (err) {return res.json(500, {err: err})} + if (!_.isEmpty(errors)) return res.json(401, {err: errors}); + if (err) return res.json(500, {err: err}); res.json(200, user); }); }; @@ -257,6 +260,8 @@ api.addTenGems = function(req, res) { }) } +// TODO delete plan + /* Setup Stripe response when posting payment */ @@ -264,20 +269,39 @@ api.buyGems = function(req, res) { var api_key = nconf.get('STRIPE_API_KEY'); var stripe = require("stripe")(api_key); var token = req.body.id; - // console.dir {token:token, req:req}, 'stripe' + var user = res.locals.user; async.waterfall([ function(cb){ - stripe.charges.create({ - amount: "500", // $5 - currency: "usd", - card: token - }, cb); + if (req.query.plan) { + stripe.customers.create({ + email: req.body.email, + metadata: {uuid: res.locals.user._id}, + card: token, + plan: req.query.plan, + }, cb); + } else { + stripe.charges.create({ + amount: "500", // $5 + currency: "usd", + card: token + }, cb); + } }, function(response, cb) { - res.locals.user.balance += 5; - res.locals.user.purchased.ads = true; - res.locals.user.save(cb); + //user.purchased.ads = true; + if (req.query.plan) { + user.purchased.plan = { + planId:'basic_earned', + customerId: response.id, + dateCreated: new Date, + dateUpdated: new Date, + gemsBought: 0 + }; + } else { + user.balance += 5; + } + user.save(cb); } ], function(err, saved){ if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors @@ -285,6 +309,29 @@ api.buyGems = function(req, res) { }); }; +api.cancelSubscription = function(req, res) { + var api_key = nconf.get('STRIPE_API_KEY'); + var stripe = require("stripe")(api_key); + var user = res.locals.user; + if (!user.purchased.plan.customerId) + return res.json(401, {err: "User does not have a plan subscription"}); + + async.waterfall([ + function(cb) { + stripe.customers.del(user.purchased.plan.customerId, cb); + }, + function(response, cb) { + user.purchased.plan = {}; + user.markModified('purchased.plan'); + user.save(cb); + } + ], function(err, saved){ + if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors + res.send(200, saved); + }); + +} + api.buyGemsPaypalIPN = function(req, res, next) { res.send(200); ipn.verify(req.body, function callback(err, msg) { @@ -298,7 +345,7 @@ api.buyGemsPaypalIPN = function(req, res, next) { if (_.isEmpty(user)) err = "user not found with uuid " + uuid + " when completing paypal transaction"; if (err) return nex(err); user.balance += 5; - user.purchased.ads = true; + //user.purchased.ads = true; user.save(); console.log('PayPal transaction completed and user updated'); }); @@ -326,6 +373,7 @@ api.cast = function(req, res) { var targetId = req.query.targetId; var klass = shared.content.spells.special[req.params.spell] ? 'special' : user.stats.class var spell = shared.content.spells[klass][req.params.spell]; + if (!spell) return res.json(404, {err: 'Spell "' + req.params.spell + '" not found.'}); var done = function(){ var err = arguments[0]; @@ -336,6 +384,7 @@ api.cast = function(req, res) { switch (targetType) { case 'task': + if (!user.tasks[targetId]) return res.json(404, {err: 'Task "' + targetId + '" not found.'}); spell.cast(user, user.tasks[targetId]); user.save(done); break; @@ -415,7 +464,7 @@ _.each(shared.wrap({}).ops, function(op,k){ api.batchUpdate = function(req, res, next) { if (_.isEmpty(req.body)) req.body = []; // cases of {} or null if (req.body[0] && req.body[0].data) - return res.json(400, {err: "API has been updated, please refresh your browser or upgrade your mobile app."}) + return res.json(501, {err: "API has been updated, please refresh your browser or upgrade your mobile app."}) var user = res.locals.user; var oldSend = res.send; @@ -423,9 +472,8 @@ api.batchUpdate = function(req, res, next) { var callOp = function(_req, cb) { res.send = res.json = function(code, data) { - if (_.isNumber(code) && code >= 400) - console.error({code: code, data: data}); - //FIXME send error messages down + 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); @@ -446,7 +494,7 @@ api.batchUpdate = function(req, res, next) { async.series(ops, function(err) { res.json = oldJson; res.send = oldSend; - if (err) return res.json(500, {err: err}); + if (err) return next(err); var response = user.toJSON(); response.wasModified = res.locals.wasModified; diff --git a/src/middleware.js b/src/middleware.js index 829e9e2cc0..19ada70aaf 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -3,6 +3,24 @@ var _ = require('lodash'); var fs = require('fs'); var path = require('path'); var User = require('./models/user').model +var limiter = require('connect-ratelimit'); + +module.exports.apiThrottle = function(app) { + app.use(limiter({ + end:false, + catagories:{ + normal: { + // 2 req/s, but split as minutes + totalRequests: 120, + every: 60000 + } + } + })).use(function(req,res,next){ + //console.log(res.ratelimit); + if (res.ratelimit.exceeded) return res.json(429,{err:'Rate limit exceeded'}); + next(); + }); +} module.exports.forceSSL = function(req, res, next){ var baseUrl = nconf.get("BASE_URL"); diff --git a/src/models/user.js b/src/models/user.js index ef8a8b3e18..506fb505b7 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -41,7 +41,8 @@ var UserSchema = new Schema({ quests: Schema.Types.Mixed, rebirths: Number, rebirthLevel: Number, - perfect: Number + perfect: Number, + habitBirthday: Boolean }, auth: { facebook: Schema.Types.Mixed, @@ -78,6 +79,13 @@ var UserSchema = new Schema({ skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true} hair: {type: Schema.Types.Mixed, 'default': {}}, shirt: {type: Schema.Types.Mixed, 'default': {}}, + plan: { + planId: String, + customerId: String, + dateCreated: Date, + dateUpdated: Date, + gemsBought: {type: Number, 'default': 0} + } }, flags: { diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 2d8bc073ab..513fb7e312 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -304,6 +304,11 @@ module.exports = (swagger, v2) -> middleware: auth.auth action:user.buyGems + "/user/cancel-subscription": + spec: method: 'POST', description: "Do not use this route" + middleware: auth.auth + action:user.cancelSubscription + "/user/buy-gems/paypal-ipn": spec: method: 'POST' diff --git a/src/server.js b/src/server.js index d5a94b1963..8d95852ad0 100644 --- a/src/server.js +++ b/src/server.js @@ -1,141 +1,148 @@ // Only do the minimal amount of work before forking just in case of a dyno restart var cluster = require("cluster"); +var _ = require('lodash'); var nconf = require('nconf'); +var utils = require('./utils'); +utils.setupConfig(); -if (false && cluster.isMaster && (nconf.get('NODE_ENV') == 'development' || nconf.get('NODE_ENV') == 'production')) { - var numCPUs = require('os').cpus().length; +var isProd = nconf.get('NODE_ENV') === 'production'; +var isDev = nconf.get('NODE_ENV') === 'development'; - // Fork workers. - for (var i = 0; i < numCPUs; i++) { - cluster.fork(); - } +if (cluster.isMaster && (isDev || isProd)) { + // Fork workers. + _.times(require('os').cpus().length, function(){ + cluster.fork(); + }) - cluster.on('exit', function(worker, code, signal) { - cluster.fork(); // replace the dead worker - }); + cluster.on('exit', 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); + }); } else { - require('coffee-script') // remove this once we've fully converted over - var express = require("express"); - var http = require("http"); - var path = require("path"); - var domainMiddleware = require('domain-middleware'); - var swagger = require("swagger-node-express"); + require('coffee-script'); // remove this once we've fully converted over + var express = require("express"); + var http = require("http"); + var path = require("path"); + var domainMiddleware = require('domain-middleware'); + var swagger = require("swagger-node-express"); - var utils = require('./utils'); - var middleware = require('./middleware'); + var middleware = require('./middleware'); - var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; - var app = express(); - var server; + var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; + var app = express(); + var server = http.createServer(); - // ------------ Setup configurations ------------ - utils.setupConfig(); - - // ------------ 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'); - }); + // ------------ 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'); + }); - // ------------ Passport Configuration ------------ - var passport = require('passport') - var util = require('util') - var FacebookStrategy = require('passport-facebook').Strategy; - // Passport session setup. - // To support persistent login sessions, Passport needs to be able to - // serialize users into and deserialize users out of the session. Typically, - // this will be as simple as storing the user ID when serializing, and finding - // the user by ID when deserializing. However, since this example does not - // have a database of user records, the complete Facebook profile is serialized - // and deserialized. - passport.serializeUser(function(user, done) { - done(null, user); - }); + // ------------ Passport Configuration ------------ + var passport = require('passport') + var util = require('util') + var FacebookStrategy = require('passport-facebook').Strategy; + // Passport session setup. + // To support persistent login sessions, Passport needs to be able to + // serialize users into and deserialize users out of the session. Typically, + // this will be as simple as storing the user ID when serializing, and finding + // the user by ID when deserializing. However, since this example does not + // have a database of user records, the complete Facebook profile is serialized + // and deserialized. + passport.serializeUser(function(user, done) { + done(null, user); + }); - passport.deserializeUser(function(obj, done) { - done(null, obj); - }); + passport.deserializeUser(function(obj, done) { + done(null, obj); + }); - // Use the FacebookStrategy within Passport. - // Strategies in Passport require a `verify` function, which accept - // credentials (in this case, an accessToken, refreshToken, and Facebook - // profile), and invoke a callback with a user object. - passport.use(new FacebookStrategy({ - clientID: nconf.get("FACEBOOK_KEY"), - clientSecret: nconf.get("FACEBOOK_SECRET"), - callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" - }, - function(accessToken, refreshToken, profile, done) { - // asynchronous verification, for effect... - //process.nextTick(function () { + // Use the FacebookStrategy within Passport. + // Strategies in Passport require a `verify` function, which accept + // credentials (in this case, an accessToken, refreshToken, and Facebook + // profile), and invoke a callback with a user object. + passport.use(new FacebookStrategy({ + clientID: nconf.get("FACEBOOK_KEY"), + clientSecret: nconf.get("FACEBOOK_SECRET"), + callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback" + }, + function(accessToken, refreshToken, profile, done) { + // asynchronous verification, for effect... + //process.nextTick(function () { - // To keep the example simple, the user's Facebook profile is returned to - // represent the logged-in user. In a typical application, you would want - // to associate the Facebook account with a user record in your database, - // and return that user instead. - return done(null, profile); - //}); - } - )); + // To keep the example simple, the user's Facebook profile is returned to + // represent the logged-in user. In a typical application, you would want + // to associate the Facebook account with a user record in your database, + // and return that user instead. + return done(null, profile); + //}); + } + )); - // ------------ Server Configuration ------------ - app.set("port", nconf.get('PORT')); + // ------------ Server Configuration ------------ - if (!process.env.SUPPRESS) app.use(express.logger("dev")); - app.use(express.compress()); - app.set("views", __dirname + "/../views"); - app.set("view engine", "jade"); - app.use(express.favicon()); - app.use(middleware.cors); - app.use(middleware.forceSSL); - app.use(express.urlencoded()); - app.use(express.json()); - app.use(express.methodOverride()); - //app.use(express.cookieParser(nconf.get('SESSION_SECRET'))); - app.use(express.cookieParser()); - app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }})); - //app.use(express.session()); + domainMiddleware({ + server: server, + killTimeout: 3000 + }), - // Initialize Passport! Also use passport.session() middleware, to support - // persistent login sessions (recommended). - app.use(passport.initialize()); - app.use(passport.session()); + app.set("port", nconf.get('PORT')); - app.use(app.router); + middleware.apiThrottle(app); + if (!isProd) app.use(express.logger("dev")); + app.use(express.compress()); + app.set("views", __dirname + "/../views"); + app.set("view engine", "jade"); + app.use(express.favicon()); + app.use(middleware.cors); + app.use(middleware.forceSSL); + app.use(express.urlencoded()); + app.use(express.json()); + app.use(express.methodOverride()); + //app.use(express.cookieParser(nconf.get('SESSION_SECRET'))); + app.use(express.cookieParser()); + app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }})); + //app.use(express.session()); - var maxAge = (nconf.get('NODE_ENV') === 'production') ? 31536000000 : 0; - app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); - app.use(express['static'](path.join(__dirname, "/../public"))); + // Initialize Passport! Also use passport.session() middleware, to support + // persistent login sessions (recommended). + app.use(passport.initialize()); + app.use(passport.session()); - // development only - //if ("development" === app.get("env")) { - // app.use(express.errorHandler()); - //} + app.use(app.router); - // Custom Directives - app.use(require('./routes/pages').middleware); - app.use(require('./routes/auth').middleware); - var v2 = express(); - app.use('/api/v2', v2); - app.use('/api/v1', require('./routes/apiv1').middleware); - app.use('/export', require('./routes/dataexport').middleware); + var maxAge = isProd ? 31536000000 : 0; + app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); + app.use(express['static'](path.join(__dirname, "/../public"))); - app.use(utils.errorHandler); + // development only + //if ("development" === app.get("env")) { + // app.use(express.errorHandler()); + //} - require('./routes/apiv2.coffee')(swagger, v2); + // Custom Directives + app.use(require('./routes/pages').middleware); + app.use(require('./routes/auth').middleware); + var v2 = express(); + app.use('/api/v2', v2); + app.use('/api/v1', require('./routes/apiv1').middleware); + app.use('/export', require('./routes/dataexport').middleware); - server = http.createServer(app).listen(app.get("port"), function() { - return console.log("Express server listening on port " + app.get("port")); - }); - app.use(domainMiddleware({ - server: server - //killTimeout: 30000, - })); -} + app.use(utils.errorHandler); + + require('./routes/apiv2.coffee')(swagger, v2); + + server.on('request', app); + server.listen(app.get("port"), function() { + return console.log("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 791ba3c57c..eacb9e7c59 100644 --- a/src/utils.js +++ b/src/utils.js @@ -56,6 +56,7 @@ module.exports.setupConfig = function(){ if (nconf.get('NODE_ENV') === "development") { Error.stackTraceLimit = Infinity; } + if (nconf.get('NODE_ENV') === 'production') require('newrelic'); }; @@ -75,7 +76,8 @@ module.exports.errorHandler = function(err, req, res, next) { text: stack }); console.error(stack); - var shortMessage = (err.message.length < 200) ? err.message : - err.message.substring(0,100) + err.message.substring(err.message.length-100,err.message.length); - res.json(500,{err:shortMessage}); //res.end(err.message); + 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 diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade index cc0c8d0d56..66bc97c7f1 100644 --- a/views/options/inventory/inventory.jade +++ b/views/options/inventory/inventory.jade @@ -78,24 +78,9 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') .popover-content p=env.t('welcomeMarket') p - button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') - =env.t('sell') - | {{selectedEgg.text}} - =env.t('for') - | {{selectedEgg.value}} - =env.t('gold') - button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') - =env.t('sell') - | {{selectedPotion.text}} - =env.t('for') - | {{selectedPotion.value}} - =env.t('gold') - button.btn.btn-primary(ng-show='selectedFood', ng-click='sellInventory()') - =env.t('sell') - | {{selectedFood.text}} - =env.t('for') - | {{selectedFood.value}} - =env.t('gold') + button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedEgg.text}}", gold: "{{selectedEgg.value}}"}) + button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedPotion.text}}", gold: "{{selectedPotion.value}}"}) + button.btn.btn-primary(ng-show='selectedFood', ng-click='sellInventory()')=env.t('sellForGold', {item: "{{selectedFood.text}}", gold: "{{selectedFood.value}}"}) menu.inventory-list(type='list') li.customize-menu menu.pets-menu(label=env.t('eggs')) @@ -115,7 +100,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html') li.customize-menu menu.pets-menu(label=env.t('food')) - div(ng-repeat='food in Content.food', ng-show='food.key !== "Saddle"') + div(ng-repeat='food in Content.food', ng-if='food.key !== "Saddle" && food.canBuy') button.customize-option(popover='{{food.notes}}', popover-title='{{food.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("food", food)', class='Pet_Food_{{food.key}}') p | {{food.value}} diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade index 457881e42c..328be90ebb 100644 --- a/views/options/inventory/stable.jade +++ b/views/options/inventory/stable.jade @@ -10,7 +10,7 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html') h3.popover-title a(target='_blank', href='http://www.kickstarter.com/profile/mattboch')=env.t('mattBoch') .popover-content - p=env.t('mattShall1') + ' {{user.profile.name}} ' + env.t('mattShall2') + p=env.t('mattShall', {name: "{{user.profile.name}}"}) h4= '{{mountCount}} / {{totalPets}} ' + env.t('mountsTamed') menu.pets(type='list') li.customize-menu(ng-repeat='egg in Content.eggs') diff --git a/views/options/profile.jade b/views/options/profile.jade index ed68f3e95d..f8e9775251 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -1,15 +1,15 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') .row-fluid .span4 - h3=env.t('bodybody') + h3=env.t('bodyBody') small | 2 / = ' ' + env.t('locked') - h5=env.t('bodysize') + h5=env.t('bodySize') .btn-group - button.btn.btn-small(ng-class='{active: user.preferences.size=="slim"}', ng-click='set({"preferences.size":"slim"})')=env.t('bodyslim') - button.btn.btn-small(ng-class='{active: user.preferences.size=="broad"}', ng-click='set({"preferences.size":"broad"})')=env.t('bodybroad') + button.btn.btn-small(ng-class='{active: user.preferences.size=="slim"}', ng-click='set({"preferences.size":"slim"})')=env.t('bodySlim') + button.btn.btn-small(ng-class='{active: user.preferences.size=="broad"}', ng-click='set({"preferences.size":"broad"})')=env.t('bodyBroad') menu(type='list') li.customize-menu @@ -17,15 +17,15 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') each shirt in ['black', 'blue', 'green', 'pink', 'white', 'yellow'] button.customize-option(class='{{user.preferences.size}}_shirt_'+shirt, type='button', ng-click='set({"preferences.shirt":"'+shirt+'"})') - menu(label=env.t('specialshirts')) + menu(label=env.t('specialShirts')) each shirt in ['convict', 'cross', 'fire', 'horizon', 'ocean', 'purple', 'rainbow', 'redblue', 'thunder', 'tropical', 'zombie'] button.customize-option(type='button', class='{{user.preferences.size}}_shirt_'+shirt, ng-class='{locked: !user.purchased.shirt.'+shirt+'}', ng-click='unlock("shirt.'+shirt+'")') menu - button.btn.btn-small.btn-primary(ng-hide="user.purchased.shirt.convict && user.purchased.shirt.cross && user.purchased.shirt.fire && user.purchased.shirt.horizon && user.purchased.shirt.ocean && user.purchased.shirt.purple && user.purchased.shirt.rainbow && user.purchased.shirt.redblue && user.purchased.shirt.thunder && user.purchased.shirt.tropical && user.purchased.shirt.zombie", ng-click='unlock("shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide="user.purchased.shirt.convict && user.purchased.shirt.cross && user.purchased.shirt.fire && user.purchased.shirt.horizon && user.purchased.shirt.ocean && user.purchased.shirt.purple && user.purchased.shirt.rainbow && user.purchased.shirt.redblue && user.purchased.shirt.thunder && user.purchased.shirt.tropical && user.purchased.shirt.zombie", ng-click='unlock("shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie")')!= env.t('unlockSet5') + ' ' .span4 - h3=env.t('bodyhead') + h3=env.t('bodyHead') small | 2 / = ' ' + env.t('locked') @@ -38,35 +38,35 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') // Special Events li.customize-menu.well.limited-edition - .label.label-info.pull-right(popover=env.t('limited31Jan'), popover-title=env.t('limitededition'), popover-placement='right', popover-trigger='mouseenter') - =env.t('limitededition') + .label.label-info.pull-right(popover=env.t('limited31Jan'), popover-title=env.t('limitedEdition'), popover-placement='right', popover-trigger='mouseenter') + =env.t('limitedEdition') | - span.glyphicon.glyphicon.icon-question-sign - menu(label=env.t('wintercolors')) + span.glyphicon.glyphicon-question-sign + menu(label=env.t('winterColors')) each color in ['candycane','frost','winternight','holly'] button(type='button', ng-class='{locked: !user.purchased.hair.color.#{color}}', class='customize-option hair_bangs_1_#{color}', style='width: 40px; height: 40px;', ng-click='unlock("hair.color.#{color}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.color.candycane && user.purchased.hair.color.frost && user.purchased.hair.color.winternight && user.purchased.hair.color.holly', ng-click='unlock("hair.color.candycane,hair.color.frost,hair.color.winternight,hair.color.holly")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.color.candycane && user.purchased.hair.color.frost && user.purchased.hair.color.winternight && user.purchased.hair.color.holly', ng-click='unlock("hair.color.candycane,hair.color.frost,hair.color.winternight,hair.color.holly")')!= env.t('unlockSet5') + ' ' - h5=env.t('bodyhair') + h5=env.t('bodyHair') // Bangs li.customize-menu - menu(label=env.t('hairbangs')) + menu(label=env.t('hairBangs')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.bangs":0})') each num in [1,2,3] button(class='hair_bangs_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-click='set({"preferences.hair.bangs":#{num}})') // Base li.customize-menu - menu(label=env.t('hairbase')) + menu(label=env.t('hairBase')) button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.base":0})') each v,k in {1:true,2:false,3:true,4:false,5:false,6:false,7:false,8:false} case v when true: button(class='hair_base_#{k}_{{user.preferences.hair.color}} customize-option', type='button', ng-click='set({"preferences.hair.base":#{k}})') when false: button(class='hair_base_#{k}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.base.#{k}}', ng-click='unlock("hair.base.#{k}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.base.2 && user.purchased.hair.base.4 && user.purchased.hair.base.5 && user.purchased.hair.base.6 && user.purchased.hair.base.7 && user.purchased.hair.base.8', ng-click='unlock("hair.base.2,hair.base.4,hair.base.5,hair.base.6,hair.base.7,hair.base.8")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.base.2 && user.purchased.hair.base.4 && user.purchased.hair.base.5 && user.purchased.hair.base.6 && user.purchased.hair.base.7 && user.purchased.hair.base.8', ng-click='unlock("hair.base.2,hair.base.4,hair.base.5,hair.base.6,hair.base.7,hair.base.8")')!= env.t('unlockSet5') + ' ' - h5=env.t('bodyfacialhair') + h5=env.t('bodyFacialHair') // Beard li.customize-menu @@ -82,31 +82,31 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') each num in [1,2] button(class='hair_mustache_#{num}_{{user.preferences.hair.color}} customize-option', type='button', ng-class='{locked: !user.purchased.hair.mustache.#{num}}', ng-click='unlock("hair.mustache.#{num}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.mustache.1 && user.purchased.hair.mustache.2 && user.purchased.hair.beard.1 && user.purchased.hair.beard.2 && user.purchased.hair.beard.3', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.hair.mustache.1 && user.purchased.hair.mustache.2 && user.purchased.hair.beard.1 && user.purchased.hair.beard.2 && user.purchased.hair.beard.3', ng-click='unlock("hair.mustache.1,hair.mustache.2,hair.beard.1,hair.beard.2,hair.beard.3")')!= env.t('unlockSet5') + ' ' .span4 - h3=env.t('bodyskin') + h3=env.t('bodySkin') small | 2 / = ' ' + env.t('locked') // skin li.customize-menu - menu(label=env.t('basicskins')) + menu(label=env.t('basicSkins')) each color in ['ddc994','f5a76e','ea8349','c06534','98461a','915533','c3e1dc','6bd049'] button.customize-option(type='button', class='skin_#{color}', ng-click='set({"preferences.skin":"#{color}"})') // Rainbow Skin - h5=env.t('rainbowskins') + h5=env.t('rainbowSkins') //menu(label='Rainbow Skins (2G / skin)') menu each color in ['eb052b','f69922','f5d70f','0ff591','2b43f6','d7a9f7','800ed0','rainbow'] button.customize-option(type='button', class='skin_#{color}', ng-class='{locked: !user.purchased.skin.#{color}}', ng-click='unlock("skin.#{color}")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock("skin.eb052b,skin.f69922,skin.f5d70f,skin.0ff591,skin.2b43f6,skin.d7a9f7,skin.800ed0,skin.rainbow")')!= env.t('unlockset5') + ' ' + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock("skin.eb052b,skin.f69922,skin.f5d70f,skin.0ff591,skin.2b43f6,skin.d7a9f7,skin.800ed0,skin.rainbow")')!= env.t('unlockSet5') + ' ' // Special Events // restore to d4df481 to see purchasing + "limited edition" code div(ng-if='user.purchased.skin.monster || user.purchased.skin.pumpkin || user.purchased.skin.skeleton || user.purchased.skin.zombie || user.purchased.skin.ghost || user.purchased.skin.shadow') - h5=env.t('spookyskins') + h5=env.t('spookySkins') menu each color in ['monster','pumpkin','skeleton','zombie','ghost','shadow'] button.customize-option(type='button', class='skin_#{color}', ng-if='user.purchased.skin.#{color}', ng-click='unlock("skin.#{color}")') @@ -116,11 +116,11 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') .border-right(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "span4" : "span6"') include ../shared/profiles/stats .span4.border-right.allocate-stats(ng-if='user.flags.classSelected && !user.preferences.disableClasses') - h3=env.t('characterbuild') + h3=env.t('characterBuild') h4 =env.t('class') + ': ' span {{ {warrior:'Warrior',wizard:'Mage',healer:'Healer',rogue:'Rogue'}[user.stats.class] }} - a.btn.btn-danger.btn-mini(ng-click='changeClass(null)')=env.t('changeclass') + a.btn.btn-danger.btn-mini(ng-click='changeClass(null)')=env.t('changeClass') small 3 table.table.table-striped tr @@ -129,48 +129,53 @@ script(id='partials/options.profile.stats.html', type='text/ng-template') {{user.stats.points}} =env.t('unallocated') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('levelpopover')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('levelPopover')) td tr td(colspan=2) fieldset.auto-allocate label.checkbox input(type='checkbox', ng-model='user.preferences.automaticAllocation', ng-change='set({"preferences.automaticAllocation": user.preferences.automaticAllocation?true: false})', ng-click='set({"preferences.allocationMode":"taskbased"})') - =env.t('automaticallocation') + =env.t('autoAllocation') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('automaticallocationpopover')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('autoAllocationPop')) form(ng-show='user.preferences.automaticAllocation',style='margin-left:1em') label.radio input(type='radio', name='allocationMode', value='flat', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "flat"})') - =env.t('evenallocation') + =env.t('evenAllocation') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('evenallocationpopover')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('evenAllocationPop')) label.radio input(type='radio', name='allocationMode', value='classbased', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "classbased"})') - =env.t('classallocation') + =env.t('classAllocation') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('classallocationpopover')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('classAllocationPop')) label.radio input(type='radio', name='allocationMode', value='taskbased', ng-model='user.preferences.allocationMode', ng-change='set({"preferences.allocationMode": "taskbased"})') - =env.t('taskallocation') + =env.t('taskAllocation') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskallocationpopover')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('taskAllocationPop')) + div(ng-show='user.preferences.automaticAllocation && !(user.preferences.allocationMode === "taskbased") && (user.stats.points > 0)') + a.btn.btn-primary.btn-mini(ng-click='user.ops.allocateNow({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('distributePointsPop')) + span.glyphicon.glyphicon-download + + =env.t('distributePoints') tr - td= env.t('allocatestr') + ' {{user.stats.str}}' + td= env.t('allocateStr') + ' {{user.stats.str}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocatestrpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("str")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateStrPop')) + tr - td= env.t('allocatecon') + ' {{user.stats.con}}' + td= env.t('allocateCon') + ' {{user.stats.con}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateconpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("con")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateConPop')) + tr - td= env.t('allocateper') + ' {{user.stats.per}}' + td= env.t('allocatePer') + ' {{user.stats.per}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateperpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("per")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocatePerPop')) + tr - td= env.t('allocateint') + ' {{user.stats.int}}' + td= env.t('allocateInt') + ' {{user.stats.int}}' td - a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateintpop')) + + a.btn.btn-primary(ng-show='user.stats.points', ng-click='allocate("int")', popover-trigger='mouseenter', popover-placement='right', popover=env.t('allocateIntPop')) + div(ng-class='user.flags.classSelected && !user.preferences.disableClasses ? "span4" : "span6"') include ../shared/profiles/achievements @@ -178,19 +183,19 @@ script(id='partials/options.profile.profile.html', type='text/ng-template') button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile')= env.t('edit') button.btn.btn-primary(ng-click='save()', ng-show='_editing.profile')= env.t('save') div(ng-show='!_editing.profile') - h4=env.t('displayname') + h4=env.t('displayName') span(ng-show='profile.profile.name') {{profile.profile.name}} span.muted(ng-hide='profile.profile.name') - =env.t('none') | - - h4=env.t('displayphoto') + h4=env.t('displayPhoto') img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') span.muted(ng-hide='profile.profile.imageUrl') - =env.t('none') | - - h4=env.t('displayblurb') + h4=env.t('displayBlurb') markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb') span.muted(ng-hide='profile.profile.blurb') - =env.t('none') @@ -200,14 +205,14 @@ script(id='partials/options.profile.profile.html', type='text/ng-template') div.whatever-options(ng-show='_editing.profile') // TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak .control-group.option-large - label.control-label=env.t('displayname') + label.control-label=env.t('displayName') input.option-content(type='text', placeholder=env.t('fullName'), ng-model='editingProfile.name') .control-group.option-large - label.control-label=env.t('photourl') - input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageurl')) + label.control-label=env.t('photoUrl') + input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageUrl')) .control-group.option-large - label.control-label=env.t('displayblurb') - textarea.option-content(style='height:15em;', placeholder=env.t("displayblurb"), ng-model='editingProfile.blurb') + label.control-label=env.t('displayBlurb') + textarea.option-content(style='height:15em;', placeholder=env.t('displayBlurb'), ng-model='editingProfile.blurb') include ../shared/formatting-help script(id='partials/options.profile.html', type="text/ng-template") @@ -217,7 +222,7 @@ script(id='partials/options.profile.html', type="text/ng-template") =env.t('avatar') li(ng-class="{ active: $state.includes('options.profile.stats') }") a(ui-sref='options.profile.stats') - =env.t('statsach') + =env.t('statsAch') li(ng-class="{ active: $state.includes('options.profile.profile') }") a(ui-sref='options.profile.profile') =env.t('profile') diff --git a/views/options/settings.jade b/views/options/settings.jade index f6f6c393ef..d084c06390 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -8,10 +8,9 @@ script(id='partials/options.settings.html', type="text/ng-template") =env.t('API') li(ng-class="{ active: $state.includes('options.settings.export') }") a(ui-sref='options.settings.export') - =env.t('dataexport') + =env.t('dataExport') li(ng-class="{ active: $state.includes('options.settings.subscription') }") - a(ui-sref='options.settings.subscription') - | Subscription + a(ui-sref='options.settings.subscription')=env.t('subscription') .tab-content .tab-pane.active @@ -28,7 +27,10 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') div small div=env.t('clockInfo') - .alert.alert-danger Warning: this is a highly-experimental feature, and many experience issues with it. + .alert.alert-danger + =env.t('subWarning1') + a(href='https://github.com/HabitRPG/habitrpg/issues/1057' target='_blank')=env.t('subWarning2') + =env.t('subWarning3') hr h4=env.t('language') select(ng-model='language.code', ng-options='lang.code as lang.name for lang in avalaibleLanguages', ng-change='changeLanguage()') @@ -38,36 +40,36 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') input(type='checkbox', ng-click='set({"preferences.hideHeader":user.preferences.hideHeader?false:true})', ng-checked='user.preferences.hideHeader!==true') =env.t('showHeader') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderpop')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('showHeaderPop')) label.checkbox input(type='checkbox', ng-click='toggleStickyHeader()', ng-checked='user.preferences.stickyHeader!==false') =env.t('stickyHeader') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderpop')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('stickyHeaderPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.newTaskEdit', ng-change='set({"preferences.newTaskEdit": user.preferences.newTaskEdit?true: false})') - =env.t('newtaskedit') + =env.t('newTaskEdit') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newtaskeditpop')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('newTaskEditPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.tagsCollapsed', ng-change='set({"preferences.tagsCollapsed": user.preferences.tagsCollapsed?true: false})') - =env.t('startcollapsed') + =env.t('startCollapsed') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startcollapsedpop')) + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startCollapsedPop')) label.checkbox input(type='checkbox', ng-model='user.preferences.advancedCollapsed', ng-change='set({"preferences.advancedCollapsed": user.preferences.advancedCollapsed?true: false})') - =env.t('startacollapsed') + =env.t('startAdvCollapsed') - span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startacollapsedpop')) - button.btn(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('showtourpop1'))= env.t('showtour') + span.glyphicon.glyphicon-question-sign(popover-trigger='mouseenter', popover-placement='right', popover=env.t('startAdvCollapsedPop')) + button.btn(ng-click='showTour()', popover-placement='right', popover-trigger='mouseenter', popover=env.t('restartTour'))= env.t('showTour') br - button.btn(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showtourpop'))= env.t('showtour2') + button.btn(ng-click='showBailey()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('showBaileyPop'))= env.t('showBailey') br - button.btn(ng-click='modals.restore = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixvalpop'))= env.t('fixval') + button.btn(ng-click='modals.restore = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('fixValPop'))= env.t('fixVal') div(ng-if='user.preferences.disableClasses==true') - button.btn(ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableclasspop'))= env.t('enableclass') + button.btn(ng-click='user.ops.changeClass({})', popover-trigger='mouseenter', popover-placement='right', popover=env.t('enableClassPop'))= env.t('enableClass') div(ng-if='!user.preferences.disableClasses && user.flags.classSelected') - button.btn(ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classtourpop'))= env.t('showclass') + button.btn(ng-click='showClassesTour()', popover-trigger='mouseenter', popover-placement='right', popover=env.t('classTourPop'))= env.t('showClass') //- Why is ng-if='user.auth.local' validating for users *without* user.auth.local (facebook users)? adding .username here for extra div(ng-if='user.auth.local.username') @@ -84,8 +86,8 @@ script(type='text/ng-template', id='partials/options.settings.settings.html') hr h4=env.t('dangerZone') - a.btn.btn-danger(ng-click='modals.reset = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetaccpop'))= env.t('resetaccount') - a.btn.btn-danger(ng-click='modals.delete = true', popover-trigger='mouseenter', popover=env.t('deleteaccpop'))= env.t('deleteaccount') + a.btn.btn-danger(ng-click='modals.reset = true', popover-trigger='mouseenter', popover-placement='right', popover=env.t('resetAccPop'))= env.t('resetAccount') + a.btn.btn-danger(ng-click='modals.delete = true', popover-trigger='mouseenter', popover=env.t('deleteAccPop'))= env.t('deleteAccount') script(type='text/ng-template', id='partials/options.settings.api.html') .row.fluid @@ -96,19 +98,19 @@ script(type='text/ng-template', id='partials/options.settings.api.html') pre.prettyprint {{user.id}} h6=env.t('APIToken') pre.prettyprint {{user.apiToken}} - h6=env.t('qrcode') + h6=env.t('qrCode') img(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7B%22address%22%3A%22https%3A%2F%2Fhabitrpg.com%22%2C%22user%22%3A%22{{user.id}}%22%2C%22key%22%3A%22{{user.apiToken}}%22%7D&choe=UTF-8&chld=L', alt='qrcode') script(id='partials/options.settings.export.html', type="text/ng-template") .row.fluid .span6 - h2=env.t('dataexport') - small=env.t('dataexports') - h4=env.t('habithistory') - =env.t('exporthistory') + h2=env.t('dataExport') + small=env.t('saveData') + h4=env.t('habitHistory') + =env.t('exportHistory') a(href="/export/history.csv")= ' ' + env.t('csv') - h4=env.t('userdata') - =env.t('exportuserdata') + h4=env.t('userData') + =env.t('exportUserData') a(href="/export/userdata.xml")= ' ' + env.t('xml') + ' ' a(href="/export/userdata.json")= env.t('json') @@ -116,28 +118,24 @@ script(id='partials/options.settings.subscription.perks.html',type='text/ng-temp table.table.table-striped tr td - span.dashed-underline(popover="Ads will stay disabled while you have an active account (original users with disabled ads are grandfathered).",popover-trigger='mouseenter',popover-placement='right') - | Disable ads + span.dashed-underline(popover=env.t('disableAdsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('disableAds') tr td - span.dashed-underline(popover="(1 Gem costs {{Shared.planGemLimits.convRate}} Gold) Addresses the \"pay to win\" concern, as everything is now achievable through hard work. There's a {{Shared.planGemLimits.convCap}}G monthly conversion cap to prevent farming.",popover-trigger='mouseenter',popover-placement='right') - | Buy Gems with Gold + span.dashed-underline(popover=env.t('buyGemsGoldText', {gemCost: "{{Shared.planGemLimits.convRate}}", gemLimit: "{{Shared.planGemLimits.convCap}}"}),popover-trigger='mouseenter',popover-placement='right')=env.t('buyGemsGold') tr td - span.dashed-underline(popover="Makes your full history available in graphs and export. Non-subscriber histories get consolidated for database optimization.",popover-trigger='mouseenter',popover-placement='right') - | Retain full history entries + span.dashed-underline(popover=env.t('retainHistoryText'),popover-trigger='mouseenter',popover-placement='right')=env.t('retainHistory') tr td - span.dashed-underline(popover="Complete your stable faster!",popover-trigger='mouseenter',popover-placement='right') - | Daily drop-caps doubled + span.dashed-underline(popover=env.t('doubleDropsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('doubleDrops') //-tr //- td +20 gems to your account tr td - span.dashed-underline(popover="This open source project can use all the help it can get. Help us keep Habit alive!",popover-trigger='mouseenter',popover-placement='right') - | Supports the developers + span.dashed-underline(popover=env.t('supportDevsText'),popover-trigger='mouseenter',popover-placement='right')=env.t('supportDevs') tr - td.alert.alert-info $5 USD / Month + td.alert.alert-info $5 + =env.t('monthUSD') script(id='partials/feature-matrix-check.html',type='text/ng-template') span.task-checker.action-yesno @@ -147,16 +145,15 @@ script(id='partials/feature-matrix-check.html',type='text/ng-template') script(id='partials/options.settings.subscription.html',type='text/ng-template') .well - h2 Individual Subscription + h2=env.t('individualSub') div(ng-if='!user.purchased.plan.customerId') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-primary(ng-click='showStripe(true)') Subscribe + .btn.btn-primary(ng-click='showStripe(true)')=env.t('subscribe') //-small.muted PayPal coming soon. div(ng-if='user.purchased.plan.customerId') .well div(style='font-size:22px') - span.glyphicon.glyphicon-ok - | Subscribed + span.glyphicon.glyphicon-ok=env.t('subscribed') div(ng-include="'partials/options.settings.subscription.perks.html'") - .btn.btn-small.btn-danger(ng-click='cancelSubscription()') Cancel Subscription \ No newline at end of file + .btn.btn-small.btn-danger(ng-click='cancelSubscription()')=env.t('cancelSub') diff --git a/views/options/social/challenges.jade b/views/options/social/challenges.jade index 08cc85c531..37b31ff9f4 100644 --- a/views/options/social/challenges.jade +++ b/views/options/social/challenges.jade @@ -1,6 +1,6 @@ script(type='text/ng-template', id='partials/options.social.challenges.detail.close.html') a.btn.btn-small.btn-danger(ng-click="delete(closingChal)")=env.t('delete') - h5=env.t('minusOr') + h5= '- ' + env.t('or') + ' -' select(ui-select2, ng-required=true, ng-model='closingChal.winner', data-placeholder=env.t('selectWinner'), ng-change='selectWinner(closingChal)', ) option(value='') option(ng-repeat='u in closingChal.members', value='{{u._id}}') {{u.profile.name}} @@ -111,7 +111,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.html') span.input-suffix.Pet_Currency_Gem1x.inline-gems span.glyphicon.glyphicon-question-sign(popover=env.t('prizePop'), popover-trigger='mouseenter', popover-placement='right') span(ng-show='newChallenge.group=="habitrpg"') - !=env.t('min1Gem') + ' ' + env.t('publicChallenges') + ' ' + env.t('helpsPrevent') + !=env.t('publicChallenges') .option-medium(ng-if='user.contributor.admin') label.checkbox diff --git a/views/options/social/group.jade b/views/options/social/group.jade index 46ba51453e..aea47a6331 100644 --- a/views/options/social/group.jade +++ b/views/options/social/group.jade @@ -11,7 +11,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild // ------ Bosses ------- .modal.inline-modal(bindonce='group', ng-if='group.type==="party" && group.quest.key') .modal-header - h3(ng-if='group.quest.active==false')=env.t('questInv') + '{{Content.quests[group.quest.key].text}}' + h3(ng-if='group.quest.active==false')=env.t('questInvitation') + '{{Content.quests[group.quest.key].text}}' h3(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text}} .modal-body div(ng-if='group.quest.active==false') @@ -61,9 +61,9 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild div(ng-if='Content.quests[group.quest.key].boss') .npc_ian.pull-left - p!=env.t('bossDmg1') + ' ' + env.t('bossDmg2') + '' + p!=env.t('bossDmg1') br - p=env.t('bossDmg3') + p=env.t('bossDmg2') div(ng-if='Content.quests[group.quest.key].collect') .npc_ian.pull-left @@ -88,7 +88,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild input.option-content(type='text', ng-model='group.name', placeholder=env.t('groupName')) .control-group.option-large label.control-label=env.t('description') - textarea.option-content(style='height:15em;', placeholder=env.t('groupdescr'), ng-model='group.description') + textarea.option-content(style='height:15em;', placeholder=env.t('groupDescr'), ng-model='group.description') .control-group.option-large label.control-label=env.t('logoUrl') input.option-content(type='url', placeholder=env.t('logoUrl'), ng-model='group.logo') @@ -131,7 +131,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild tr(ng-if='group.memberCount > group.members.length') td |{{group.memberCount - group.members.length}} - = ' ' + env.t('moremembers') + = ' ' + env.t('moreMembers') | h4(ng-show='group.invites.length > 0')=env.t('invited') diff --git a/views/options/social/hall.jade b/views/options/social/hall.jade index 780c64dd65..f494892d2c 100644 --- a/views/options/social/hall.jade +++ b/views/options/social/hall.jade @@ -42,9 +42,9 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html') .control-group.option-medium input.option-content(type='number', step="any", ng-model='hero.balance') - span.input-suffix Balance + span.input-suffix=env.t('balance') p - small!= '`user.balance`' + env.t('USD1') + ' ' + env.t('not') + ' ' + env.t('USD2') + small!= '`user.balance`' + env.t('notGems') .control-group.option-medium label.checkbox @@ -70,7 +70,7 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html') span(ng-if='hero.contributor.admin',popover=env.t('gamemaster'),popover-trigger='mouseenter',popover-placement='right') a.label(class='label-contributor-{{hero.contributor.level}}', ng-class='{"label-npc": hero.backer.npc}', ng-click='clickMember(hero._id, true)') | {{hero.profile.name}} - span.glyphicon.glyphicon-star.icon-white + span.glyphicon.glyphicon-star span(ng-if='!hero.contributor.admin') a.label(class='label-contributor-{{hero.contributor.level}}', ng-class='{"label-npc": hero.backer.npc}', ng-click='clickMember(hero._id, true)') {{hero.profile.name}} td(ng-if='user.contributor.admin') {{hero._id}} diff --git a/views/options/social/tavern.jade b/views/options/social/tavern.jade index 1193639838..349065f739 100644 --- a/views/options/social/tavern.jade +++ b/views/options/social/tavern.jade @@ -68,35 +68,40 @@ a.label.label-contributor-1(ng-click='toggleUserTier($event)')=env.t('friendBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('first') + ' ' + env.t('friend1Text') + ' 2' + env.t('gems') + '.' + span.achievement.achievement-firefox + !=env.t('friendFirst') hr p - != ' ' + env.t('friendText') + ' ' + env.t('second') + ' ' + env.t('friendText2') + ' ' + env.t('crystalArmor') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_armor_special_1') + !=env.t('friendSecond') tr td a.label.label-contributor-3(ng-click='toggleUserTier($event)')=env.t('eliteBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('third') + ' ' + env.t('friendText2') + ' ' + env.t('crystalHelmet') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_head_special_1') + !=env.t('eliteThird') hr p - != ' ' + env.t('friendText') + ' ' + env.t('fourth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalSword') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' - + span.shop-sprite.item-img(class='shop_weapon_special_1') + !=env.t('eliteFourth') tr td a.label.label-contributor-5(ng-click='toggleUserTier($event)')=env.t('championBadge') div(style='display:none;') p - != ' ' + env.t('friendText') + ' ' + env.t('fifth') + ' ' + env.t('friendText2') + ' ' + env.t('crystalShield') + ' ' + env.t('friendText3') + ' 2' + env.t('gems') + '.' + span.shop-sprite.item-img(class='shop_shield_special_1') + !=env.t('championFifth') hr p - != '
' + env.t('friendText') + ' ' + env.t('sixth') + ' ' + env.t('friendText2b') + ' ' + env.t('hydraPet') + ' ' + env.t('friendText3b') + ' 2' + env.t('gems') + '.' + div(class='Pet-Dragon-Hydra pull-left') + !=env.t('championSixth') tr td a.label.label-contributor-7(ng-click='toggleUserTier($event)')=env.t('legendaryBadge') div(style='display:none;') p - != env.t('friendText') + ' ' + env.t('seventh') + ' ' + env.t('friendText2c') + ' 2' + env.t('gems') + ' ' + env.t('friendText3c') + !=env.t('legSeventh') tr td a.label.label-contributor-8(ng-click='toggleUserTier($event)')=env.t('heroicBadge') @@ -111,12 +116,12 @@ include ./challenge-box .span8(ng-controller='ChatCtrl') - h3=env.t('taverntalk') + h3=env.t('tavernTalk') include ./chat-box small(style='position: relative; top: -20px; left: 25px;') include ../../shared/formatting-help small(style='position: relative; top: 18px; left: 0px') alert.alert-info - !=env.t('tavernalert1') + ' ' + env.t('tavernalert2') + '.' + !=env.t('tavernAlert1') + ' ' + env.t('tavernAlert2') + '.' ul.unstyled.tavern-chat include ./chat-message diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 6acd65c1a8..fa54dcc12a 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -44,7 +44,7 @@ figure.herobox(ng-click='spell ? castEnd(profile, "user", $event) : clickMember( // FIXME handle @minimal, this might have to be a directive span.current-pet(class='Pet-{{profile.items.currentPet}}', ng-show='profile.items.currentPet && !minimal') .avatar-level(ng-class='userLevelStyle(profile,"label")') - span.glyphicon.glyphicon-circle-arrow-up.icon-white(ng-show='profile.stats.buffs.str || profile.stats.buffs.per || profile.stats.buffs.con || profile.stats.buffs.int || profile.stats.buffs.stealth', tooltip=env.t('buffed'), style='margin-right:5px;') + span.glyphicon.glyphicon-circle-arrow-up(ng-show='profile.stats.buffs.str || profile.stats.buffs.per || profile.stats.buffs.con || profile.stats.buffs.int || profile.stats.buffs.stealth', tooltip=env.t('buffed'), style='margin-right:5px;') =env.t('lvl') | {{profile.stats.lvl}} - span.glyphicon.glyphicon-plus-sign.icon-white(ng-show='profile.achievements.rebirths', tooltip=env.t('reborn') + '{{profile.achievements.rebirthLevel}}', style='margin-left:5px') + span.glyphicon.glyphicon-plus-sign(ng-show='profile.achievements.rebirths', tooltip=env.t('reborn', {reLevel: "{{profile.achievements.rebirthLevel}}"}), style='margin-left:5px') diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 4810f0c69b..d02a9fa8c2 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -10,19 +10,19 @@ span.glyphicon.glyphicon-ok =env.t('tasks') span(ng-show='$state.includes("tasks")', ui-sref='options') - span.glyphicon.glyphicon.icon-wrench + span.glyphicon.glyphicon-wrench =env.t('options') li a.task-action-btn.tile.solid(href="http://habitrpg.wikia.com/wiki/FAQ", target='_blank') - span.glyphicon.glyphicon.icon-book + span.glyphicon.glyphicon.-book =env.t('FAQ') li a.task-action-btn.tile.solid(href="https://vimeo.com/57654086", target='_blank') - span.glyphicon.glyphicon.icon-film + span.glyphicon.glyphicon-film =env.t('tutorials') li a.task-action-btn.tile.solid(ng-click='User.sync()') - span.glyphicon.glyphicon.icon-refresh + span.glyphicon.glyphicon-refresh =env.t('sync') li a.task-action-btn.tile.solid(ng-click='logout()') diff --git a/views/shared/modals/achievements.jade b/views/shared/modals/achievements.jade index b679a24c28..9ea49bd5c9 100644 --- a/views/shared/modals/achievements.jade +++ b/views/shared/modals/achievements.jade @@ -37,11 +37,8 @@ script(id='modals/achievements/contributor.html', type='text/ng-template') .modal-body .npc_justin.float-left p - | {{user.profile.name}}, - =env.t('contribText1') - | {{user.contributor.level}} - =env.t('contribText2') - a(href='http://habitrpg.wikia.com/wiki/Contributor_Rewards' target='_blank')=env.t('contribText3') + !=env.t('contribModal', {name: "{{user.profile.name}}", level: "{{user.contributor.level}}"}) + a(href='http://habitrpg.wikia.com/wiki/Contributor_Rewards' target='_blank')=env.t('contribLink') .modal-footer button.btn.btn-default.cancel(ng-click='set({"flags.contributor":false})')=env.t('ok') @@ -51,10 +48,6 @@ script(id='modals/achievements/rebirth.html', type='text/ng-template') h3=env.t('modalAchievement') .modal-body .achievement.achievement-sun - =env.t('rebirthAchievement1') - | {{user.achievements.rebirths}} - =env.t('rebirthAchievement2') - | {{user.achievements.rebirthLevel}}. - =env.t('rebirthAchievement3') + =env.t('rebirthAchievement', {number: "{{user.achievements.rebirths}}", level: "{{user.achievements.rebirthLevel}}"}) .modal-footer button.btn.btn-default.cancel(ng-click='modals.achievements.rebirth = false')=env.t('ok') diff --git a/views/shared/modals/buy-gems.jade b/views/shared/modals/buy-gems.jade index 2c9d42e268..b43b1dc6ea 100644 --- a/views/shared/modals/buy-gems.jade +++ b/views/shared/modals/buy-gems.jade @@ -4,7 +4,7 @@ script(id='modals/buyGems.html', type='text/ng-template') .buy-gems include ../gems .well - h3 Buy Gems + h3=env.t('buyGems') table.table tr td @@ -15,7 +15,8 @@ script(id='modals/buyGems.html', type='text/ng-template') span.dashed-underline(popover=env.t('donateText3'),popover-trigger='mouseenter',popover-placement='right') =env.t('donateText2') tr - td.alert.alert-info=env.t('fiveUSD1') + td.alert.alert-info $5 + =env.t('USD') tr td .btn.btn-primary(ng-click='showStripe()',style='margin-left:10px;')=env.t('payWithCard') diff --git a/views/shared/modals/new-stuff.jade b/views/shared/modals/new-stuff.jade index c5623d18b0..e849afd403 100644 --- a/views/shared/modals/new-stuff.jade +++ b/views/shared/modals/new-stuff.jade @@ -30,24 +30,50 @@ script(type='text/ng-template', id='modals/newStuff.html') table.table.table-striped tr td - h5 Group Plans - p We've begun adding plans for groups (parents, teachers, health & wellness administrators, etc). These plans will provide group leaders with more control, privacy, security, and support. Currently only the Organization Plan (top tier) is available (due to tech limitations believe it or not), and we'll be releasing the Family & Group plans later. Click the "Contact Us" buttons if you're interested, and we'll keep you updated! + h5 Happy Birthday, HabitRPG! + p The fair land of Habitica is two years old on January 31st! The NPCs are celebrating in style, and it looks like some of the staff is, too! Won't you join in? tr td - h5 Individual Plan - p We've introduced a $5/mo basic subscription plan. It comes with a number of perks, which you can see here. We'll likely add more benefits over time, follow the conversation here. + h5 Absurd Party Robtes + p As part of the festivities, Absurd Party Robes are available free of charge in the Item Store! Swath yourself in those silly garbs and don your matching hats to celebrate this momentous day. tr td - h5 Perfect Day Achievement - p Now when you complete all your dailies, you stack this badge, plus and additional perk: you get a +(level/2) buff to all stats! + h5 Delicious Cake + p What would a birthday be without birthday cake in a myriad of flavors? Of course, pets are very picky, but luckily Lemoness and her team of bakers have plenty of slices to go around. Mmm, delicious! tr td - h5 Spread The Word Challenge Update - p We have 2k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. + h5 Last Day of Winter Wonderland Event + p Also, just a reminder - January 31st is the final day of the Winter Wonderland event, so it's your last day to get the Limited Edition Winter Hair Colors, the Winter Outfits, the snowballs, and the Trapper Santa and Find the Cub quest scrolls. Remember that mid-progress Trapper Santa and Find the Cub quests will not abort, nor will you lose your scrolls - they will simply be removed from Alexander's marketplace. We hope that you've had a wonderful winter! + tr + td + h5 Birthday Bash Badge + p Finally, to commemorate the fun, all party participants receive a birthday badge! Polish it frequently and wear it fondly. + p Thanks so much for being a part of the HabitRPG community. We love you guys, and we can't wait to have you at our sides in the upcoming year! Stay productive, Habiteers, and have an awesome day. + p.muted By @lemoness - small.muted 01/28/2014 + small.muted 01/30/2014 + + hr + h5 01/28/2014 + table.table.table-striped + tr + td + h5 Group Plans + p We've begun adding plans for groups (parents, teachers, health & wellness administrators, etc). These plans will provide group leaders with more control, privacy, security, and support. Currently only the Organization Plan (top tier) is available (due to tech limitations believe it or not), and we'll be releasing the Family & Group plans later. Click the "Contact Us" buttons if you're interested, and we'll keep you updated! + tr + td + h5 Individual Plan + p We've introduced a $5/mo basic subscription plan. It comes with a number of perks, which you can see here. We'll likely add more benefits over time, follow the conversation here. + tr + td + h5 Perfect Day Achievement + p Now when you complete all your dailies, you stack this badge, plus and additional perk: you get a +(level/2) buff to all stats! + tr + td + h5 Spread The Word Challenge Update + p We have 1k+ submissions, holy cow! Great job everyone! Now, we need to go through these manually, so it will take a few days to a couple weeks to process. The challenge will stay open until we're done choosing our winners, but be sure to edit the To-Do with your submission URL before 1/31, as that's the cut-off date for processing. We'll send a Tweet out when the winner has been selected, so follow @habitrpg and stay tuned. hr h5 01/25/2014 diff --git a/views/shared/modals/settings.jade b/views/shared/modals/settings.jade index 1de8fac436..cd714e9dfa 100644 --- a/views/shared/modals/settings.jade +++ b/views/shared/modals/settings.jade @@ -2,7 +2,7 @@ div(modal='modals.reset') script(type='text/ng-template', id='modals/reset.html') .modal-header - h3=env.t('resetaccount') + h3=env.t('resetAccount') .modal-body p=env.t('resetText1') p=env.t('resetText2') @@ -67,11 +67,9 @@ script(type='text/ng-template', id='modals/restore.html') //div(modal='modals.delete') script(type='text/ng-template', id='modals/delete.html') .modal-header - h3=env.t('deleteaccount') + h3=env.t('deleteAccount') .modal-body - p=env.t('deleteText1') - strong=env.t('deleteText2') - =env.t('deleteText3') + p!=env.t('deleteText') p input(type='text', ng-model='_deleteAccount') .modal-footer diff --git a/views/shared/profiles/achievements.jade b/views/shared/profiles/achievements.jade index a2628a7801..2ef6789a86 100644 --- a/views/shared/profiles/achievements.jade +++ b/views/shared/profiles/achievements.jade @@ -123,3 +123,11 @@ small =env.t('annoyingFriendsText', {snowballs: "{{profile.achievements.snowball}}"}) hr + + div(ng-if='profile.achievements.habitBirthday') + .achievement.achievement-habitBirthday + h5=env.t('habitBirthday') + small + =env.t('habitBirthdayText') + hr + diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 5ef7d8b3a5..16911930e7 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -93,9 +93,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s a(ng-click='unlink(task, "remove-all")')=env.t('removeThem') div(ng-if='task.challenge.broken=="CHALLENGE_CLOSED"') p - =env.t('challengeCompleted1') - span.badge {{task.challenge.winner}} - =env.t('challengeCompleted2') + !=env.t('challengeCompleted', {user: "{{task.challenge.winner}}"}) p a(ng-click='unlink(task, "keep-all")')=env.t('keepThem') | | @@ -117,7 +115,7 @@ li(bindonce='list', bo-id='"task-"+task.id', ng-repeat='task in obj[list.type+"s legend.option-title =env.t('checklist') | - span.glyphicon.glyphicon.icon-question-sign(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom') + span.glyphicon.glyphicon-question-sign(popover=env.t('checklistText'),popover-trigger='mouseenter',popover-placement='bottom') ul.unstyled li(ng-repeat='item in task.checklist') a.pull-right.checklist-icons(ng-click='removeChecklistItem(task,$event,$index,true)') diff --git a/views/static/about.jade b/views/static/about.jade index 1e635d1bc3..f5748b182b 100644 --- a/views/static/about.jade +++ b/views/static/about.jade @@ -5,7 +5,7 @@ block vars - var menuItem = 'about' block title - title About + title=env.t('companyAbout') block content .row diff --git a/views/static/contact.jade b/views/static/contact.jade index 43591138b2..129734d3de 100644 --- a/views/static/contact.jade +++ b/views/static/contact.jade @@ -3,7 +3,7 @@ block vars - var layoutEnv = env - var menuItem = 'contact' block title - title Contact + title=env.t('contact') block content // Probably just add linkks to the respective contact locations? // Bugs? Github diff --git a/views/static/layout.jade b/views/static/layout.jade index dea3788eed..5c5e04c2d3 100644 --- a/views/static/layout.jade +++ b/views/static/layout.jade @@ -28,13 +28,13 @@ html .collapse.navbar-collapse(collapse="isNavbarCollapsed") ul.nav.navbar-nav li(class='#{menuItem=="about" ? "active" : ""}') - a(href='/static/about') Learn More + a(href='/static/about')=env.t('learnMore') li - a(href='http://blog.habitrpg.com/') Blog + a(href='http://blog.habitrpg.com/')=env.t('companyBlog') li(class='#{menuItem=="plans" ? "active" : ""}') - a(href='/static/plans') Plans + a(href='/static/plans')=env.t('groupPlans') //li(class='#{menuItem=="contact" ? "active" : ""}') - a(href='/static/contact') Contact + a(href='/static/contact')=env.t('contact') button#header-play-button.btn.btn-primary.navbar-btn.navbar-right(ng-click='playButtonClick()')=env.t('playButton') diff --git a/views/static/plans.jade b/views/static/plans.jade index 3fc613e1f0..e062d398e2 100644 --- a/views/static/plans.jade +++ b/views/static/plans.jade @@ -5,29 +5,37 @@ block vars - var menuItem = 'plans' block title - title Group Plans + title=env.t('groupPlans') block content .row .col-md-12 - h2 Group Plans + h2=env.t('groupPlans') - p For individuals, HabitRPG is free to play. Even for small interest groups, free (or cheap) guilds and challenges can be used to motivate participants in behavioral modification. Think writing groups, art challenges, and more. - p But some group leaders will want more control, privacy, security, and support. Examples of such groups are families, health and wellness groups, employee groups, and more. These plans provide private instances of HabitRPG for your group or organization, secure and independent of Habitica. See below for additional plan perks, and contact us for more information! + p + =env.t('indivPlan1') + a(href='http://habitrpg.wikia.com/wiki/Guilds' target='_blank')=env.t('guilds') + = ' ' + env.t('and') + ' ' + a(href='http://habitrpg.wikia.com/wiki/Challenges' target='_blank')=env.t('challenges') + =env.t('indivPlan2') + p + =env.t('groupText1') + a(href='http://habitrpg.wikia.com/wiki/Habitica' target='_blank')=env.t('habitica') + |. + =env.t('groupText2') .subscription-features table.table.table-striped thead tr th.feature-name - th.feature-name.muted Family (Coming Soon) - th.feature-name.muted Group (Coming Soon) - th.feature-name Organization + th.feature-name.muted=env.t('planFamily') + th.feature-name.muted=env.t('planGroup') + th.feature-name=env.t('organization') tbody tr th - span.dashed-underline(popover="Members of the organization participate outside of HabitRPG proper, providing focus for your participants.",popover-trigger='mouseenter',popover-placement='right') - | Private Organization + span.dashed-underline(popover=env.t('organizationSubText'),popover-trigger='mouseenter',popover-placement='right')=env.t('organizationSub') td.muted span.glyphicon.glyphicon-ok td.muted @@ -36,53 +44,46 @@ block content span.glyphicon.glyphicon-ok tr th - span.dashed-underline(popover="Dedicated Hosting: you get your own database and server hosted by HabitRPG, or optionally we'll install it in your organization's network. If not checked, the plan uses \"Shared Hosting\": your organization uses the same database as HabitRPG proper while performing independently Habitica. Your members are shielded from Tavern & Guilds, but still on the same server/database.",popover-trigger='mouseenter',popover-placement='right') - | Dedicated Hosting + span.dashed-underline(popover=env.t('dedicatedHostText'),popover-trigger='mouseenter',popover-placement='right')=env.t('dedicatedHost') td td td span.glyphicon.glyphicon-ok tr th - span.dashed-underline(popover="We can optionally give you your own domain for the installation.",popover-trigger='mouseenter',popover-placement='right') - | Custom Domain + span.dashed-underline(popover=env.t('customDomainText'),popover-trigger='mouseenter',popover-placement='right')=env.t('customDomain') td td td span.glyphicon.glyphicon-ok tr th - span.dashed-underline(popover="The maximum number of players in your private organization.",popover-trigger='mouseenter',popover-placement='right') - | Max Participants + span.dashed-underline(popover=env.t('maxPlayersText'),popover-trigger='mouseenter',popover-placement='right')=env.t('maxPlayers') td.muted 10 td.muted 75 - td Unlimited + td=env.t('unlimited') tr th - span.dashed-underline(popover="First to be provided for with support.",popover-trigger='mouseenter',popover-placement='right') - | Priority Support On Tickets & Hosting + span.dashed-underline(popover=env.t('priSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('priSupport') td td td span.glyphicon.glyphicon-ok tr th - span.dashed-underline(popover="We will provide support for training, bugs, installation, and feature requests. Additional hours available at an hourly rate.",popover-trigger='mouseenter',popover-placement='right') - | Support Hours / Month + span.dashed-underline(popover=env.t('timeSupportText'),popover-trigger='mouseenter',popover-placement='right')=env.t('timeSupport') td.muted - td.muted 5 td 10 tr th - h5 Game features: + h5=env.t('gameFeatures') + ':' ul - li Ads disabled for members + li=env.t('gameNoAds') li - span.dashed-underline(popover="Members will be able to purchase gems with gold, meaning none of your participants need to buy anything with real money.",popover-trigger='mouseenter',popover-placement='right') - | Gems purchasable with gold + span.dashed-underline(popover=env.t('gold2GemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('gold2Gem') li - span.dashed-underline(popover="We will provide the organization leaders with as many gems as they need, for things like challenge prizes, guild-creation, etc.",popover-trigger='mouseenter',popover-placement='right') - | Infinite leader gems + span.dashed-underline(popover=env.t('infiniteGemText'),popover-trigger='mouseenter',popover-placement='right')=env.t('infiniteGem') td.muted span.glyphicon.glyphicon-ok td.muted @@ -93,8 +94,8 @@ block content th //| Price td - a.btn(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover="Plan not yet available, but click to contact us and we'll keep you updated.",popover-placement='right',popover-trigger='mouseenter') Contact Us + a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover=env.t('notYetPlan'),popover-placement='right',popover-trigger='mouseenter')=env.t('contactUs') td - a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover="Plan not yet available, but click to contact us and we'll keep you updated.",popover-placement='right',popover-trigger='mouseenter') Contact Us + a.btn.muted(href='https://docs.google.com/forms/d/17torT7OlxgtbHAPdBDRQNG2lTdIQyrGXk4O2YXvUMig/viewform',popover=env.t('notYetPlan'),popover-placement='right',popover-trigger='mouseenter')=env.t('contactUs') td - a.btn.btn-primary(href='https://docs.google.com/forms/d/12Jqj_8f3oQS0B3ZUHewHbK61uLjBdzBeB0zyEqB9lxM/viewform') Contact Us \ No newline at end of file + a.btn.btn-primary(href='https://docs.google.com/forms/d/12Jqj_8f3oQS0B3ZUHewHbK61uLjBdzBeB0zyEqB9lxM/viewform')=env.t('contactUs') \ No newline at end of file