Merge branch 'develop' of https://github.com/HabitRPG/habitrpg into develop

This commit is contained in:
Matteo Pagliazzi 2014-05-07 18:40:02 +02:00
commit facdca48b0
35 changed files with 342 additions and 309 deletions

View file

@ -30,7 +30,7 @@
"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",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git",
"BrowserQuest": "git://github.com/mozilla/BrowserQuest.git",
"github-buttons": "git://github.com/mdo/github-buttons.git",
"marked": "~0.2.9",

View file

@ -4,7 +4,7 @@
"version": "0.0.0-152",
"main": "./src/server.js",
"dependencies": {
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#develop",
"habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared",
"connect-mongo": "*",
"express": "3.5.x",
"gzippo": "*",

View file

@ -213,7 +213,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
"public": Groups.publicGuilds()
}
$scope.type = 'guild';
$scope.text = 'Guild';
$scope.text = window.env.t('guild');
var newGroup = function(){
return new Groups.Group({type:'guild', privacy:'private'});
}
@ -316,7 +316,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
.controller("PartyCtrl", ['$rootScope','$scope', 'Groups', 'User', 'Challenges', '$state', '$compile',
function($rootScope,$scope, Groups, User, Challenges, $state, $compile) {
$scope.type = 'party';
$scope.text = 'Party';
$scope.text = window.env.t('party');
$scope.group = $rootScope.party = Groups.party();
$scope.newGroup = new Groups.Group({type:'party'});

View file

@ -104,7 +104,7 @@ habitrpg.controller("InventoryCtrl", ['$rootScope', '$scope', '$window', 'User',
var food = $scope.selectedFood
if (food.key == 'Saddle') {
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}))) {
} 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}});
@ -124,7 +124,7 @@ 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(window.env.t('mustComplete', {quest: $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(window.env.t('mustLevel', {level: item.lvl}));
$rootScope.selectedQuest = item;

View file

@ -204,11 +204,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 = window.env.t('youCast', {spell: spell.text});
var msg = window.env.t('youCast', {spell: spell.text()});
switch (type) {
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;
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);
});

View file

@ -12,9 +12,5 @@ if(window.moment && window.env.language && window.env.language.momentLang && win
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);
}
window.habitrpgShared.i18n.strings = window.env.translations;
window.env.t = window.habitrpgShared.i18n.t;

View file

@ -78,6 +78,13 @@ angular.module('guideServices', []).
});
$('.main-herobox').popover('destroy');
var tour = new Tour({
template: "<div class='popover'>" +
"<div class='arrow'></div><h3 class='popover-title'></h3><div class='popover-content'></div>" +
"<div class='popover-navigation'><div class='btn-group'>" +
"<button class='btn btn-sm btn-default' data-role='prev'>&laquo;</button>" +
"<button class='btn btn-sm btn-default' data-role='next'>&raquo;</button>" +
"<button class='btn btn-sm btn-default' data-role='pause-resume' data-pause-text='Pause' data-resume-text='Resume'>Pause</button></div>" +
"<button class='btn btn-sm btn-default' data-role='end'>" + window.env.t('endTour') + "</button></div></div>",
onEnd: function(){
User.set({'flags.showTour': false});
}

View file

@ -88,6 +88,8 @@ api.registerUser = function(req, res, next) {
timestamps: {created: +new Date(), loggedIn: +new Date()}
}
};
newUser.preferences = newUser.preferences || {};
newUser.preferences.language = req.language; // User language detected from browser, not saved
user = new User(newUser);
// temporary for conventions
@ -255,7 +257,11 @@ api.setupPassport = function(router) {
},
function(user, cb){
if (user) return cb(null, user);
user = new User({
preferences: {
language: req.language // User language detected from browser, not saved
},
auth: {
facebook: req.user,
timestamps: {created: +new Date(), loggedIn: +new Date()}

View file

@ -76,7 +76,7 @@ api.score = function(req, res, next) {
if (task.type === 'daily' || task.type === 'todo')
task.completed = direction === 'up';
}
var delta = user.ops.score({params:{id:task.id, direction:direction}});
var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language});
user.save(function(err,saved){
if (err) return next(err);
@ -336,7 +336,7 @@ api.cast = function(req, res, next) {
if (group) {
series.push(function(cb2){
var message = '`'+user.profile.name+' casts '+spell.text + (targetType=='user' ? ' on '+found.profile.name : ' for the party')+'.`';
var message = '`'+user.profile.name+' casts '+spell.text() + (targetType=='user' ? ' on '+found.profile.name : ' for the party')+'.`';
group.sendChat(message);
group.save(cb2);
})
@ -397,6 +397,8 @@ api.batchUpdate = function(req, res, next) {
res.locals.ops = [];
var ops = _.transform(req.body, function(m,_req){
if (_.isEmpty(_req)) return;
_req.language = req.language;
m.push(function() {
var cb = arguments[arguments.length-1];
res.locals.ops.push(_req);

97
src/i18n.js Normal file
View file

@ -0,0 +1,97 @@
var fs = require('fs'),
path = require('path'),
_ = require('lodash'),
User = require('./models/user').model,
shared = require('habitrpg-shared'),
translations = {};
var loadTranslations = function(locale){
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');
fs.readdirSync(path.join(__dirname, "/../node_modules/habitrpg-shared/locales/")).forEach(function(file) {
if(file === 'en' || file === 'README.md') return;
loadTranslations(file);
// Merge missing strings from english
_.defaults(translations[file], translations.en);
});
var langCodes = Object.keys(translations);
var avalaibleLanguages = _.map(langCodes, function(langCode){
return {
code: langCode,
name: translations[langCode].languageName
}
});
// Load MomentJS localization files
var momentLangs = {};
// Handle different language codes from MomentJS and /locales
var momentLangsMapping = {
'en': 'en-gb',
'no': 'nn'
};
var momentLangs = {};
_.each(langCodes, function(code){
var lang = _.find(avalaibleLanguages, {code: code});
lang.momentLangCode = (momentLangsMapping[code] || code);
try{
// MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files
var f = fs.readFileSync(path.join(__dirname, '/../node_modules/moment/lang/' + lang.momentLangCode + '.js'), 'utf8');
momentLangs[code] = f;
}catch (e){}
});
var getUserLanguage = function(req, res, next){
var getFromBrowser = function(){
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';
};
var getFromUser = function(user){
var lang;
if(user && user.preferences.language && translations[user.preferences.language]){
lang = user.preferences.language;
}else{
var preferred = getFromBrowser();
lang = translations[preferred] ? preferred : 'en';
}
req.language = lang;
next();
};
if(req.locals && req.locals.user){
getFromUser(req.locals.user);
}else if(req.session && req.session.userId){
User.findOne({_id: req.session.userId}, function(err, user){
if(err) return callback(err);
getFromUser(user);
});
}else{
getFromUser(null);
}
};
shared.i18n.translations = translations;
module.exports = {
translations: translations,
avalaibleLanguages: avalaibleLanguages,
langCodes: langCodes,
getUserLanguage: getUserLanguage,
momentLangs: momentLangs
};

View file

@ -7,6 +7,8 @@ var limiter = require('connect-ratelimit');
var logging = require('./logging');
var domainMiddleware = require('domain-middleware');
var cluster = require('cluster');
var i18n = require('./i18n.js');
var shared = require('habitrpg-shared');
module.exports.apiThrottle = function(app) {
if (nconf.get('NODE_ENV') !== 'production') return;
@ -139,121 +141,34 @@ var getManifestFiles = function(page){
return code;
}
// Translations
var translations = {};
var loadTranslations = function(locale){
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');
fs.readdirSync(path.join(__dirname, "/../node_modules/habitrpg-shared/locales/")).forEach(function(file) {
if(file === 'en') return;
loadTranslations(file);
// Merge missing strings from english
_.defaults(translations[file], translations.en);
});
var langCodes = Object.keys(translations);
var avalaibleLanguages = _.map(langCodes, function(langCode){
return {
code: langCode,
name: translations[langCode].languageName
}
});
// Load MomentJS localization files
var momentLangs = {};
// Handle different language codes from MomentJS and /locales
var momentLangsMapping = {
'en': 'en-gb',
'no': 'nn'
};
var momentLangs = {};
_.each(langCodes, function(code){
var lang = _.find(avalaibleLanguages, {code: code});
lang.momentLangCode = (momentLangsMapping[code] || code);
try{
// MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files
var f = fs.readFileSync(path.join(__dirname, '/../node_modules/moment/lang/' + lang.momentLangCode + '.js'), 'utf8');
momentLangs[code] = f;
}catch (e){}
});
var getUserLanguage = function(req, callback){
var getFromBrowser = function(){
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';
};
if(req.session && req.session.userId){
User.findOne({_id: req.session.userId}, function(err, user){
if(err) return callback(err);
if(user && user.preferences.language && translations[user.preferences.language]){
return callback(null, _.find(avalaibleLanguages, {code: user.preferences.language}));
}else{
var langCode = getFromBrowser();
// 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()}));
}
}
module.exports.locals = function(req, res, next) {
getUserLanguage(req, function(err, language){
if(err) return res.json(500, {err: err});
var language = _.find(i18n.avalaibleLanguages, {code: req.language});
var isStaticPage = req.url.split('/')[1] === 'static'; // If url contains '/static/'
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 && i18n.momentLangs[language.code]) || undefined);
// Load moment.js language file only when not on static pages
language.momentLang = ((!isStaticPage && momentLangs[language.code])|| undefined);
res.locals.habitrpg = {
NODE_ENV: nconf.get('NODE_ENV'),
BASE_URL: nconf.get('BASE_URL'),
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: i18n.avalaibleLanguages,
language: language,
isStaticPage: isStaticPage,
translations: i18n.translations[language.code],
t: function(){ // stringName and vars are the allowed parameters
var args = Array.prototype.slice.call(arguments, 0);
args.push(language.code);
return shared.i18n.t.apply(null, args);
},
siteVersion: siteVersion
}
res.locals.habitrpg = {
NODE_ENV: nconf.get('NODE_ENV'),
BASE_URL: nconf.get('BASE_URL'),
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);
},
siteVersion: siteVersion
}
next();
});
next();
}
module.exports.enTranslations = function(stringName, vars){

View file

@ -305,14 +305,14 @@ var UserSchema = new Schema({
id: { type: String, 'default': shared.uuid },
name: String,
challenge: String
}], 'default': shared.content.userDefaults.tags},
}]},
challenges: [{type: 'String', ref:'Challenge'}],
habits: {type:[TaskSchemas.HabitSchema], 'default': shared.content.userDefaults.habits},
dailys: {type:[TaskSchemas.DailySchema], 'default': shared.content.userDefaults.dailys},
todos: {type:[TaskSchemas.TodoSchema], 'default': shared.content.userDefaults.todos},
rewards: {type:[TaskSchemas.RewardSchema], 'default': shared.content.userDefaults.rewards},
habits: {type:[TaskSchemas.HabitSchema]},
dailys: {type:[TaskSchemas.DailySchema]},
todos: {type:[TaskSchemas.TodoSchema]},
rewards: {type:[TaskSchemas.RewardSchema]},
extra: Schema.Types.Mixed
@ -348,6 +348,31 @@ UserSchema.post('init', function(doc){
UserSchema.pre('save', function(next) {
// Populate new users with default content
if (this.isNew){
//TODO for some reason this doesn't work here: `_.merge(this, shared.content.userDefaults);`
var self = this;
_.each(['habits', 'dailys', 'todos', 'rewards', 'tags'], function(taskType){
self[taskType] = _.map(shared.content.userDefaults[taskType], function(task){
var newTask = task;
// Render task's text and notes in user's language
if(taskType === 'tags'){
// tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
newTask.id = shared.uuid();
newTask.name = task.name(self.preferences.language);
}else{
newTask.text = task.text(self.preferences.language);
newTask.notes = task.notes(self.preferences.language);
}
return newTask;
});
});
this.preferences.language = undefined;
}
//this.markModified('tasks');
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
this.preferences.dayStart = 0;

View file

@ -19,6 +19,7 @@ middleware = require("../middleware")
cron = user.cron
_ = require('lodash')
content = require('habitrpg-shared').content
i18n = require('../i18n')
module.exports = (swagger, v2) ->
@ -47,7 +48,7 @@ module.exports = (swagger, v2) ->
spec:
description: "Export user history"
method: 'GET'
middleware: auth.auth
middleware: [auth.auth, i18n.getUserLanguage]
action: dataexport.history #[todo] encode data output options in the data controller and use these to build routes
# ---------------------------------
@ -138,7 +139,7 @@ module.exports = (swagger, v2) ->
path("id", "Task ID", "string")
query 'keep',"When unlinking a challenge task, how to handle the orphans?",'string',['keep','keep-all','remove','remove-all']
]
middleware: auth.auth ## removing cron since they may want to remove task first
middleware: [auth.auth, i18n.getUserLanguage] ## removing cron since they may want to remove task first
action: challenges.unlink
@ -227,7 +228,7 @@ module.exports = (swagger, v2) ->
path: '/user'
method: 'DELETE'
description: "Delete a user object entirely, USE WITH CAUTION!"
middleware: auth.auth
middleware: [auth.auth, i18n.getUserLanguage]
action: user["delete"]
"/user/revive":
@ -306,7 +307,7 @@ module.exports = (swagger, v2) ->
parameters:[
body '','The array of batch-operations to perform','object'
]
middleware: [middleware.forceRefresh, auth.auth, cron]
middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron]
action: user.batchUpdate
# Tags
@ -350,7 +351,7 @@ module.exports = (swagger, v2) ->
parameters: [
query 'type',"Comma-separated types of groups to return, eg 'party,guilds,public,tavern'",'string'
]
middleware: auth.auth
middleware: [auth.auth, i18n.getUserLanguage]
action: groups.list
@ -362,7 +363,7 @@ module.exports = (swagger, v2) ->
parameters: [
body '','Group object (see GroupSchema)','object'
]
middleware: auth.auth
middleware: [auth.auth, i18n.getUserLanguage]
action: groups.create
"/groups/{gid}:GET":
@ -370,7 +371,7 @@ module.exports = (swagger, v2) ->
path: '/groups/{gid}'
description: "Get a group"
parameters: [path('gid','Group ID','string')]
middleware: auth.auth
middleware: [auth.auth, i18n.getUserLanguage]
action: groups.get
"/groups/{gid}:POST":
@ -379,7 +380,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: "Edit a group"
parameters: [body('','Group object (see GroupSchema)','object')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.update
"/groups/{gid}/join":
@ -387,7 +388,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: 'Join a group'
parameters: [path('gid','Id of the group to join','string')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.join
"/groups/{gid}/leave":
@ -395,7 +396,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: 'Leave a group'
parameters: [path('gid','ID of the group to leave','string')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.leave
"/groups/{gid}/invite":
@ -406,7 +407,7 @@ module.exports = (swagger, v2) ->
path 'gid','Group id','string'
query 'uuid','User id to invite','string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action:groups.invite
"/groups/{gid}/removeMember":
@ -417,7 +418,7 @@ module.exports = (swagger, v2) ->
path 'gid','Group id','string'
query 'uuid','User id to boot','string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action:groups.removeMember
"/groups/{gid}/questAccept":
@ -428,7 +429,7 @@ module.exports = (swagger, v2) ->
path 'gid',"Group id",'string'
query 'key',"optional. if provided, trigger new invite, if not, accept existing invite",'string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action:groups.questAccept
"/groups/{gid}/questReject":
@ -438,7 +439,7 @@ module.exports = (swagger, v2) ->
parameters: [
path 'gid','Group id','string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questReject
"/groups/{gid}/questAbort":
@ -446,7 +447,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: 'Abort quest'
parameters: [path('gid','Group to abort quest in','string')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.questAbort
#TODO PUT /groups/:gid/chat/:messageId
@ -456,7 +457,7 @@ module.exports = (swagger, v2) ->
path: "/groups/{gid}/chat"
description: "Get all chat messages"
parameters: [path('gid','Group to return the chat from ','string')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.getChat
@ -469,7 +470,7 @@ module.exports = (swagger, v2) ->
query 'message', 'Chat message','string'
path 'gid','Group id','string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.postChat
# placing before route below, so that if !=='seen' it goes to next()
@ -480,7 +481,7 @@ module.exports = (swagger, v2) ->
parameters: [
path 'gid','Group id','string'
]
middleware: []
middleware: [i18n.getUserLanguage]
action: groups.seenMessage
"/groups/{gid}/chat/{messageId}":
@ -488,7 +489,7 @@ module.exports = (swagger, v2) ->
method: 'DELETE'
description: 'Delete a group'
parameters: [path('gid','ID of group to delete','string')]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.deleteChatMessage
"/groups/{gid}/chat/{mid}/like":
@ -499,7 +500,7 @@ module.exports = (swagger, v2) ->
path 'gid','Group id','string'
path 'mid','Message id','string'
]
middleware: [auth.auth, groups.attachGroup]
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
action: groups.likeChatMessage
# ---------------------------------
@ -514,19 +515,19 @@ module.exports = (swagger, v2) ->
# ---------------------------------
"/hall/heroes":
spec: {}
middleware:[auth.auth]
middleware:[auth.auth, i18n.getUserLanguage]
action: hall.getHeroes
"/hall/heroes/{uid}:GET":
spec: path: "/hall/heroes/{uid}"
middleware:[auth.auth, hall.ensureAdmin]
middleware:[auth.auth, i18n.getUserLanguage, hall.ensureAdmin]
action: hall.getHero
"/hall/heroes/{uid}:POST":
spec:
method: 'POST'
path: "/hall/heroes/{uid}"
middleware: [auth.auth, hall.ensureAdmin]
middleware: [auth.auth, i18n.getUserLanguage, hall.ensureAdmin]
action: hall.updateHero
"/hall/patrons":
@ -534,7 +535,7 @@ module.exports = (swagger, v2) ->
parameters: [
query 'page','Page number to fetch (this list is long)','string'
]
middleware:[auth.auth]
middleware:[auth.auth, i18n.getUserLanguage]
action: hall.getPatrons
@ -549,7 +550,7 @@ module.exports = (swagger, v2) ->
spec:
path: '/challenges'
description: "Get a list of challenges"
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.list
@ -559,7 +560,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: "Create a challenge"
parameters: [body('','Challenge object (see ChallengeSchema)','object')]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.create
"/challenges/{cid}:GET":
@ -584,7 +585,7 @@ module.exports = (swagger, v2) ->
path 'cid','Challenge id','string'
body('','Challenge object (see ChallengeSchema)','object')
]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.update
"/challenges/{cid}:DELETE":
@ -593,7 +594,7 @@ module.exports = (swagger, v2) ->
method: 'DELETE'
description: "Delete a challenge"
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges["delete"]
"/challenges/{cid}/close":
@ -604,7 +605,7 @@ module.exports = (swagger, v2) ->
path 'cid','Challenge id','string'
query 'uid','User ID of the winner','string',true
]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.selectWinner
"/challenges/{cid}/join":
@ -612,7 +613,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: "Join a challenge"
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.join
"/challenges/{cid}/leave":
@ -620,7 +621,7 @@ module.exports = (swagger, v2) ->
method: 'POST'
description: 'Leave a challenge'
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.leave
"/challenges/{cid}/member/{uid}":
@ -630,7 +631,7 @@ module.exports = (swagger, v2) ->
path 'cid','Challenge id','string'
path 'uid','User id','string'
]
middleware: [auth.auth]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.getMember
@ -662,7 +663,7 @@ module.exports = (swagger, v2) ->
#type: 'Pet'
errorResponses: []
method: 'GET'
route.middleware ?= if path.indexOf('/user') is 0 then [auth.auth, cron] else []
route.middleware ?= if path.indexOf('/user') is 0 then [auth.auth, i18n.getUserLanguage, cron] else []
swagger["add#{route.spec.method}"](route);true

View file

@ -1,17 +1,18 @@
var auth = require('../controllers/auth');
var express = require('express');
var i18n = require('../i18n');
var router = new express.Router();
/* auth.auth*/
auth.setupPassport(router); //FIXME make this consistent with the others
router.post('/api/v2/register', auth.registerUser);
router.post('/api/v2/user/auth/local', auth.loginLocal);
router.post('/api/v2/user/auth/facebook', auth.loginFacebook);
router.post('/api/v2/user/reset-password', auth.resetPassword);
router.post('/api/v2/user/change-password', auth.auth, auth.changePassword);
router.post('/api/v2/register', i18n.getUserLanguage, auth.registerUser);
router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
router.post('/api/v2/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook);
router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword);
router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword);
router.post('/api/v1/register', auth.registerUser);
router.post('/api/v1/user/auth/local', auth.loginLocal);
router.post('/api/v1/user/auth/facebook', auth.loginFacebook);
router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser);
router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
router.post('/api/v1/user/auth/facebook', i18n.getUserLanguage, auth.loginFacebook);
module.exports = router;

View file

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

View file

@ -5,9 +5,10 @@ var _ = require('lodash');
var middleware = require('../middleware');
var user = require('../controllers/user');
var auth = require('../controllers/auth');
var i18n = require('../i18n');
// -------- App --------
router.get('/', middleware.locals, function(req, res) {
router.get('/', i18n.getUserLanguage, middleware.locals, function(req, res) {
if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
return res.redirect('/static/front')
@ -19,35 +20,35 @@ router.get('/', middleware.locals, function(req, res) {
// -------- Marketing --------
router.get('/static/front', middleware.locals, function(req, res) {
router.get('/static/front', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/front', {env: res.locals.habitrpg});
});
router.get('/static/privacy', middleware.locals, function(req, res) {
router.get('/static/privacy', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/privacy', {env: res.locals.habitrpg});
});
router.get('/static/terms', middleware.locals, function(req, res) {
router.get('/static/terms', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/terms', {env: res.locals.habitrpg});
});
router.get('/static/api', middleware.locals, function(req, res) {
router.get('/static/api', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/api', {env: res.locals.habitrpg});
});
router.get('/static/features', middleware.locals, function(req, res) {
router.get('/static/features', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/features', {env: res.locals.habitrpg});
});
router.get('/static/videos', middleware.locals, function(req, res) {
router.get('/static/videos', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/videos', {env: res.locals.habitrpg});
});
router.get('/static/contact', middleware.locals, function(req, res) {
router.get('/static/contact', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/contact', {env: res.locals.habitrpg});
});
router.get('/static/plans', middleware.locals, function(req, res) {
router.get('/static/plans', i18n.getUserLanguage, middleware.locals, function(req, res) {
res.render('static/plans', {env: res.locals.habitrpg});
});

View file

@ -27,6 +27,9 @@ if (cluster.isMaster && (isDev || isProd)) {
var swagger = require("swagger-node-express");
var autoinc = require('mongoose-id-autoinc');
// Setup translations
var i18n = require('./i18n');
var middleware = require('./middleware');
var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14;

View file

@ -28,30 +28,4 @@ describe('Root Controller', function() {
expect(scope.contribText({level: 8, text: 'Blacksmith'}, {npc: 'NPC'})).to.eql('NPC');
});
describe('i18n', function(){
var t = env.t;
beforeEach(function(){
env.translations = {
"text1": 'translatedText1',
"text2": '<%= number %> eggs',
"stringNotFound" : "String '<%= string %>' not found.",
};
});
it('translates strings', function(){
expect(t('')).to.eql("String '' not found.");
expect(t('text1')).to.eql('translatedText1');
expect(t(' text1 ')).to.eql("String ' text1 ' not found.");
expect(t('text3')).to.eql("String 'text3' not found.");
});
it('interpolates strings', function(){
expect(t('text2', {number: 2})).to.eql('2 eggs');
expect(t('text2', {number: 'ten'})).to.eql('ten eggs');
expect(t('text2', {n: 10, number: 'ten'})).to.eql('ten eggs');
expect(t('text2')).to.eql('<%= number %> eggs');
});
});
});

View file

@ -4,9 +4,9 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
.col-md-6.border-right
h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('battleGearText'))=env.t('battleGear')
li.customize-menu.inventory-gear
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {warrior:"Warrior", wizard:"Mage", rogue:"Rogue", healer:"Healer", special:"Special", mystery:"Mystery"}', ng-show='gear[klass]')
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery")}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}')
button.customize-option(popover='{{item.notes()}}', popover-title='{{item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.equipped[item.type] == item.key}')
.col-md-6
h3.equipment-title.hint(popover-trigger='mouseenter', popover-placement='right', popover=env.t('costumeText'))=env.t('costume')
.checkbox.equipment-title
@ -15,9 +15,9 @@ script(type='text/ng-template', id='partials/options.inventory.equipment.html')
|&nbsp;
=env.t('useCostume')
li.customize-menu(ng-if='user.preferences.costume')
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {warrior:"Warrior", wizard:"Mage", rogue:"Rogue", healer:"Healer", special:"Special", mystery:"Mystery"}', ng-show='gear[klass]')
menu.pets-menu(label='{{label}}', ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery")}', ng-show='gear[klass]')
div(ng-repeat='item in gear[klass]')
button.customize-option(popover='{{item.notes}}', popover-title='{{item.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
button.customize-option(popover='{{item.notes()}}', popover-title='{{item.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='user.ops.equip({params:{type:"costume", key:item.key}})', class='shop_{{item.key}}', ng-class='{selectableInventory: user.items.gear.costume[item.type] == item.key}')
script(type='text/ng-template', id='partials/options.inventory.drops.html')
.container-fluid
@ -32,36 +32,36 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
p.muted(ng-show='eggCount < 1')=env.t('noEggs')
div(ng-repeat='(egg,points) in ownedItems(user.items.eggs)')
//TODO move positioning this styling to css
button.customize-option(popover='{{Content.eggs[egg].notes}}', popover-title='{{Content.eggs[egg].text}} Egg', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseEgg(egg)', class='Pet_Egg_{{egg}}', ng-class='{selectableInventory: selectedPotion && !(user.items.pets[egg+"-"+selectedPotion.key]>0)}')
button.customize-option(popover='{{Content.eggs[egg].notes()}}', popover-title!=env.t("egg", {eggType: "{{Content.eggs[egg].text()}}"}), popover-trigger='mouseenter', popover-placement='right', ng-click='chooseEgg(egg)', class='Pet_Egg_{{egg}}', ng-class='{selectableInventory: selectedPotion && !(user.items.pets[egg+"-"+selectedPotion.key]>0)}')
.badge.badge-info.stack-count {{points}}
//-p {{Content.eggs[egg].text}}
//-p {{Content.eggs[egg].text()}}
li.customize-menu
menu.hatchingPotion-menu(label=(env.t('hatchingPotions') + ' ({{potCount}})'))
p.muted(ng-show='potCount < 1')=env.t('noHatchingPotions')
div(ng-repeat='(pot,points) in ownedItems(user.items.hatchingPotions)')
button.customize-option(popover='{{Content.hatchingPotions[pot].notes}}', popover-title='{{Content.hatchingPotions[pot].text}} Potion', popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !(user.items.pets[selectedEgg.key+"-"+pot]>0)}')
button.customize-option(popover='{{Content.hatchingPotions[pot].notes()}}', popover-title!=env.t("potion", {potionType: "{{Content.hatchingPotions[pot].text()}}"}), popover-trigger='mouseenter', popover-placement='right', ng-click='choosePotion(pot)', class='Pet_HatchingPotion_{{pot}}', ng-class='{selectableInventory: selectedEgg && !(user.items.pets[selectedEgg.key+"-"+pot]>0)}')
.badge.badge-info.stack-count {{points}}
li.customize-menu
menu.pets-menu(label=(env.t('scrolls') + ' ({{questCount}})'))
menu.pets-menu(label=(env.t('quests') + ' ({{questCount}})'))
p.muted(ng-show='questCount < 1')=env.t('noScrolls')
p.muted!=env.t('scrollsText1') + ' <a href="/#/options/groups/party">' + env.t('scrollsText2') + '</a>'
div(ng-repeat='(quest_key,points) in ownedItems(user.items.quests)', ng-init='quest = Content.quests[quest_key]')
button.customize-option(popover="{{quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes | htmlDecode}}", popover-title='{{quest.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='showQuest(quest_key)', class='inventory_quest_scroll', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}')
button.customize-option(popover="{{quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes() | htmlDecode}}", popover-title='{{quest.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='showQuest(quest_key)', class='inventory_quest_scroll', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}')
.badge.badge-info.stack-count {{points}}
li.customize-menu
menu.pets-menu(label=env.t('food') + ' ({{foodCount}})')
p(ng-show='foodCount < 1')=env.t('noFood')
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover='{{Content.food[food].notes}}', popover-title='{{Content.food[food].text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{food}}')
button.customize-option(popover='{{Content.food[food].notes()}}', popover-title='{{Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='chooseFood(food)', class='Pet_Food_{{food}}')
.badge.badge-info.stack-count {{points}}
li.customize-menu(ng-if='user.items.special.snowball')
menu.pets-menu(label=env.t('special'))
div
button.customize-option(popover='{{Content.special.snowball.notes}}', popover-title='{{Content.special.snowball.text}}', popover-trigger='mouseenter', popover-placement='right', ng-click='castStart(Content.special.snowball)', class='inventory_special_snowball')
button.customize-option(popover='{{Content.special.snowball.notes()}}', popover-title='{{Content.special.snowball.text()}}', popover-trigger='mouseenter', popover-placement='right', ng-click='castStart(Content.special.snowball)', class='inventory_special_snowball')
.badge.badge-info.stack-count {{user.items.special.snowball}}
li.customize-menu(ng-if='user.items.special.valentineReceived[0]')
@ -91,29 +91,29 @@ 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('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}}"})
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'))
div(ng-repeat='egg in Content.eggs', ng-if='egg.canBuy')
button.customize-option(popover='{{egg.notes}}', popover-title='{{egg.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", egg)', class='Pet_Egg_{{egg.key}}')
button.customize-option(popover='{{egg.notes()}}', popover-title!=env.t("egg", {eggType: "{{egg.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", egg)', class='Pet_Egg_{{egg.key}}')
p
| {{egg.value}}
span.Pet_Currency_Gem1x.inline-gems
div(ng-show='user.achievements.quests.gryphon > 1')
button.customize-option(popover='{{Content.eggs.Gryphon.notes}}', popover-title='{{Content.eggs.Gryphon.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Gryphon)', class='Pet_Egg_Gryphon')
button.customize-option(popover='{{Content.eggs.Gryphon.notes()}}', popover-title!=env.t("egg", {eggType: "{{Content.eggs.Gryphon.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Gryphon)', class='Pet_Egg_Gryphon')
p
| {{Content.eggs.Gryphon.value}}
span.Pet_Currency_Gem1x.inline-gems
div(ng-show='user.achievements.quests.hedgehog > 1')
button.customize-option(popover='{{Content.eggs.Hedgehog.notes}}', popover-title='{{Content.eggs.Hedgehog.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Hedgehog)', class='Pet_Egg_Hedgehog')
button.customize-option(popover='{{Content.eggs.Hedgehog.notes()}}', popover-title!=env.t("egg", {eggType: "{{Content.eggs.Hedgehog.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Hedgehog)', class='Pet_Egg_Hedgehog')
p
| {{Content.eggs.Hedgehog.value}}
span.Pet_Currency_Gem1x.inline-gems
div(ng-show='user.achievements.quests.ghost_stag > 1')
button.customize-option(popover='{{Content.eggs.Deer.notes}}', popover-title='{{Content.eggs.Deer.text}} Egg', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Deer)', class='Pet_Egg_Deer')
button.customize-option(popover='{{Content.eggs.Deer.notes()}}', popover-title!=env.t("egg", {eggType: "{{Content.eggs.Deer.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("eggs", Content.eggs.Deer)', class='Pet_Egg_Deer')
p
| {{Content.eggs.Deer.value}}
span.Pet_Currency_Gem1x.inline-gems
@ -121,7 +121,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
li.customize-menu
menu.pets-menu(label=env.t('hatchingPotions'))
div(ng-repeat='pot in Content.hatchingPotions')
button.customize-option(popover='{{pot.notes}}', popover-title='{{pot.text}} Potion', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("hatchingPotions", pot)', class='Pet_HatchingPotion_{{pot.key}}')
button.customize-option(popover='{{pot.notes()}}', popover-title!=env.t("potion", {potionType: "{{pot.text()}}"}), popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("hatchingPotions", pot)', class='Pet_HatchingPotion_{{pot.key}}')
p
| {{pot.value}}
span.Pet_Currency_Gem1x.inline-gems
@ -129,7 +129,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-if='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}}')
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}}
span.Pet_Currency_Gem1x.inline-gems
@ -138,7 +138,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
menu.pets-menu(label=env.t('quests'))
p.muted!=env.t('scrollsText1') + ' <a href="/#/options/groups/party">' + env.t('scrollsText2') + '</a>'
div(ng-repeat='quest in Content.quests', ng-if='quest.canBuy')
button.customize-option(popover="{{quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes | htmlDecode}}", popover-title='{{quest.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='buyQuest(quest.key)', class='inventory_quest_scroll', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}')
button.customize-option(popover="{{quest.previous && !user.achievements.quests[quest.previous] ? env.t('scrollsPre') : quest.notes() | htmlDecode}}", popover-title='{{quest.text()}}', popover-trigger='mouseenter', popover-placement='left', ng-click='buyQuest(quest.key)', class='inventory_quest_scroll', ng-class='{locked: quest.previous && !user.achievements.quests[quest.previous]}')
p
| {{quest.value}}
span.Pet_Currency_Gem1x.inline-gems
@ -151,7 +151,7 @@ script(type='text/ng-template', id='partials/options.inventory.drops.html')
| 4
span.Pet_Currency_Gem1x.inline-gems
//-div
button.customize-option(popover='{{Content.spells.special.snowball.notes}}', popover-title='{{Content.spells.special.snowball.text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("special", Content.spells.special.snowball)', class='inventory_special_snowball')
button.customize-option(popover='{{Content.spells.special.snowball.notes()}}', popover-title='{{Content.spells.special.snowball.text()}}', popover-trigger='mouseenter', popover-placement='left', ng-click='purchase("special", Content.spells.special.snowball)', class='inventory_special_snowball')
p
| {{Content.spells.special.snowball.value}}
span.Pet_Currency_Gem1x.inline-gems

View file

@ -15,7 +15,7 @@ script(type='text/ng-template', id='partials/options.inventory.mounts.html')
menu.pets(type='list')
li.customize-menu(ng-repeat='egg in Content.eggs')
menu
div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.mountText}}', popover-placement='bottom', ng-init='mount = egg.key+"-"+potion.key')
div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover=env.t('mountName', {potion: '{{potion.text()}}', mount: '{{egg.mountText()}}'}), popover-placement='bottom', ng-init='mount = egg.key+"-"+potion.key')
button(class="pet-button Mount_Head_{{mount}}", ng-show='user.items.mounts[mount]', ng-class='{active: user.items.currentMount == mount}', ng-click='chooseMount(egg.key, potion.key)')
//div(class='Mount_Head_{{mount}}')
button(class="pet-button pet-not-owned", ng-hide='user.items.mounts[mount]')
@ -33,7 +33,7 @@ mixin petList(source)
menu.pets(type='list')
li.customize-menu(ng-repeat='egg in #{source}')
menu
div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover='{{potion.text}} {{egg.text}}', popover-placement='bottom', ng-init='pet = egg.key+"-"+potion.key')
div(ng-repeat='potion in Content.hatchingPotions', popover-trigger='mouseenter', popover=env.t('petName', {potion: '{{potion.text()}}', egg: '{{egg.text()}}'}), popover-placement='bottom', ng-init='pet = egg.key+"-"+potion.key')
button(class="pet-button Pet-{{pet}}", ng-if='user.items.pets[pet]>0', ng-class='{active: user.items.currentPet == pet, selectableInventory: selectedFood}', ng-click='choosePet(egg.key, potion.key)')
.progress(ng-show='!user.items.mounts[pet] && egg.key!="Egg"')
.progress-bar.progress-bar-success(style="width:{{user.items.pets[pet]/.5}}%")
@ -85,7 +85,7 @@ script(type='text/ng-template', id='partials/options.inventory.pets.html')
li.customize-menu
menu.pets-menu(label=env.t('food'))
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
button.customize-option(popover-append-to-body='true', popover='{{Content.food[food].notes}}', popover-title='{{Content.food[food].text}}', popover-trigger='mouseenter', popover-placement='left', ng-click='chooseFood(food)', class='Pet_Food_{{food}}')
button.customize-option(popover-append-to-body='true', popover='{{Content.food[food].notes()}}', popover-title='{{Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='left', ng-click='chooseFood(food)', class='Pet_Food_{{food}}')
.badge.badge-info.stack-count {{points}}
// Remove this once we have images in
p {{Content.food[food].text}}
p {{Content.food[food].text()}}

View file

@ -81,7 +81,7 @@ mixin customizeProfile(mobile)
// Flower
li.customize-menu
menu(label="Flower")
menu(label=env.t('flower'))
button(class='head_0 customize-option', type='button', ng-click='set({"preferences.hair.flower":0})')
each num in [1,2,3,4,5,6]
button(class='hair_flower_#{num} customize-option', type='button', ng-click='set({"preferences.hair.flower":#{num}})')
@ -141,7 +141,7 @@ script(id='partials/options.profile.stats.html', type='text/ng-template')
h3=env.t('characterBuild')
h4
=env.t('class') + ': '
span {{ {warrior:'Warrior',wizard:'Mage',healer:'Healer',rogue:'Rogue'}[user.stats.class] }}&nbsp;
span {{ {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer")}[user.stats.class] }}&nbsp;
a.btn.btn-danger.btn-xs(ng-click='changeClass(null)')=env.t('changeClass')
small.cost 3 <span class="Pet_Currency_Gem1x inline-gems"/>
table.table.table-striped

View file

@ -4,8 +4,7 @@
h3.panel-title=env.t('challenges')
&nbsp;
a.pull-right(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58')
small
="More info"
small=env.t('moreInfo')
.panel-body.modal-fixed-height(bindonce='group.challenges')
div(ng-if='group.challenges.length > 0')
table.table.table-striped

View file

@ -107,7 +107,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
input.form-control(type='text', minlength="3", maxlength="16", ng-model='newChallenge.shortName', placeholder=env.t('challengeTag'), required)
&nbsp;
a.hint.vertical-20(target='_blank', href='http://habitrpg.wikia.com/wiki/Tags', popover=env.t('challengeTagPop'), popover-trigger='mouseenter', popover-placement='right')
="More info"
=env.t('moreInfo')
.form-group
textarea.form-control(cols='3', placeholder=env.t('challengeDescr'), ng-model='newChallenge.description')
@ -121,7 +121,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.html')
span.input-suffix.Pet_Currency_Gem1x.inline-gems
&nbsp;
span.hint.vertical-20(popover=env.t('prizePop'), popover-trigger='mouseenter', popover-placement='right')
="More info"
=env.t('moreInfo')
span(ng-show='newChallenge.group=="habitrpg"')
!=env.t('publicChallenges')

View file

@ -2,10 +2,8 @@
.row
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
.form-group
label.control-label(for='new-group-name')
| {{text}}&nbsp;
=env.t('name')
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=('{{text}} ' + env.t('name')), ng-model='newGroup.name')
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
.form-group
label(for='new-group-description')=env.t('description')
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')

View file

@ -12,8 +12,8 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild
// ------ Bosses -------
.panel.panel-default(bindonce='group', ng-if='group.type==="party" && group.quest.key')
.panel-heading
h3.panel-title(ng-if='group.quest.active==false')=env.t('questInvitation') + ' {{Content.quests[group.quest.key].text}}'
h3.panel-title(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text}}
h3.panel-title(ng-if='group.quest.active==false')=env.t('questInvitation') + ' {{Content.quests[group.quest.key].text()}}'
h3.panel-title(ng-if='group.quest.active==true') {{Content.quests[group.quest.key].text()}}
.panel-body.modal-fixed-height
div(ng-if='group.quest.active==false')
table.table.table-striped
@ -50,11 +50,11 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title=env.t('guild
tr(ng-repeat='(k,v) in group.quest.progress.collect', class='quest_collected_{{v >= Content.quests[group.quest.key].collect[k].count}}')
td
div.pull-left(class='quest_{{group.quest.key}}_{{k}}')
| {{Content.quests[group.quest.key].collect[k].text}}
| {{Content.quests[group.quest.key].collect[k].text()}}
td
{{v}} / {{Content.quests[group.quest.key].collect[k].count}}
div(ng-bind-html='Content.quests[group.quest.key].notes')
div(ng-bind-html='Content.quests[group.quest.key].notes()')
hr
h5=env.t('participants')
table.table.table-striped

View file

@ -23,7 +23,9 @@ script(type='text/ng-template', id='partials/options.social.party.html')
{{user.id}}
p
=env.t('startLFG')
|&nbsp;
a(href="http://habitrpg.wikia.com/wiki/Board:The_Archery_-_LFG")=env.t('linkLFG')
|&nbsp;
=env.t('endLFG')
include ./create-group

View file

@ -1,4 +1,3 @@
//Why is shown? Because of script here at line 24!!!!!!!!!
script(id='modals/buyGems.html', type='text/ng-template')
.modal-body
.buy-gems

View file

@ -5,7 +5,7 @@ script(type='text/ng-template', id='modals/dropsEnabled.html')
p
figure
.item-drop-icon(class='Pet_Egg_Wolf')
span!=env.t('firstDrop', {eggText: "{{Content.eggs.Wolf.text}}", eggNotes: "{{Content.eggs.Wolf.notes}}"})
span!=env.t('firstDrop', {eggText: "{{Content.eggs.Wolf.text()}}", eggNotes: "{{Content.eggs.Wolf.notes()}}"})
br
p
figure

View file

@ -1,6 +1,6 @@
script(type='text/ng-template', id='modals/newStuff.html')
.modal-header
h4 New Stuff!
h4=env.t('newStuff')
.modal-body.new-stuff-modal.modal-fixed-height
table
tr
@ -723,5 +723,5 @@ script(type='text/ng-template', id='modals/newStuff.html')
a(href='https://trello.com/card/custom-day-start/50e5d3684fe3a7266b0036d6/15') Add custom day start
.modal-footer
button.btn.btn-default(ng-click='$close()') Cool
button.btn.btn-warning(ng-click='dismissAlert(); $close()') Dismiss This Alert
button.btn.btn-default(ng-click='$close()')=env.t('cool')
button.btn.btn-warning(ng-click='dismissAlert(); $close()')=env.t('dismissAlert')

View file

@ -3,7 +3,7 @@ script(id='partials/options.social.party.quest-rewards.html', type='text/ng-temp
h5 {{header}}
table.table.table-striped
tr(ng-repeat='drop in quest.drop.items')
td {{drop.text}}
td {{drop.text()}}
tr
td {{quest.drop.exp}}
=env.t('experience')
@ -13,20 +13,20 @@ script(id='partials/options.social.party.quest-rewards.html', type='text/ng-temp
script(type='text/ng-template', id='modals/questCompleted.html')
.modal-header
h4 "{{Content.quests[user.party.quest.completed].text}}"
h4 "{{Content.quests[user.party.quest.completed].text()}}"
=env.t('completed')
.modal-body
p {{Content.quests[user.party.quest.completed].completion}}
p {{Content.quests[user.party.quest.completed].completion()}}
quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived'))
.modal-footer
button.btn.btn-primary(ng-click='set({"party.quest.completed":""}); $close()')=env.t('accept')
script(type='text/ng-template', id='modals/showQuest.html')
.modal-header
h4 {{selectedQuest.text}}
h4 {{selectedQuest.text()}}
.modal-body
.pull-right(class='quest_{{selectedQuest.key}}')
div(ng-bind-html='selectedQuest.notes')
div(ng-bind-html='selectedQuest.notes()')
quest-rewards(key='{{selectedQuest.key}}')
hr
.npc_ian.pull-left
@ -38,10 +38,10 @@ script(type='text/ng-template', id='modals/showQuest.html')
script(type='text/ng-template', id='modals/buyQuest.html')
.modal-header
h4 {{selectedQuest.text}}
h4 {{selectedQuest.text()}}
.modal-body
.pull-right(class='quest_{{selectedQuest.key}}')
div(ng-bind-html='selectedQuest.notes')
div(ng-bind-html='selectedQuest.notes()')
quest-rewards(key='{{selectedQuest.key}}')
.modal-footer
button.btn.btn-default(ng-click='closeQuest(); $close()')=env.t('neverMind')
@ -50,9 +50,9 @@ script(type='text/ng-template', id='modals/buyQuest.html')
script(type='text/ng-template', id='modals/questInvitation.html')
.modal-header
h4=env.t('questInvitation')
|&nbsp;{{Content.quests[party.quest.key].text}}
|&nbsp;{{Content.quests[party.quest.key].text()}}
.modal-body
div(ng-bind-html='Content.quests[party.quest.key].notes')
div(ng-bind-html='Content.quests[party.quest.key].notes()')
quest-rewards(key='{{party.quest.key}}')
.modal-footer
button.btn.btn-default(ng-click='questHold = true; $close()')=env.t('askLater')

View file

@ -28,6 +28,7 @@ div(ng-if='profile.contributor.level || user._id == profile._id')
span.label.label-default(ng-if='!profile.contributor.level')=env.t('contribName')
small
=env.t('contribText')
|&nbsp;
a(href='http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG', target='_blank')=env.t('readMore')
| .
hr

View file

@ -22,12 +22,12 @@ table.table.table-striped(ng-show='user.flags.itemsEnabled')
// FIXME this isn't refreshing properly. On first party modal, correct. Second party modal click gives the previous gear. It's always one behind.
tr(ng-repeat='(k,v) in profile.items.gear.equipped', ng-init='piece=Content.gear.flat[v]', ng-show='piece')
td
strong {{piece.text}}:&nbsp;
strong {{piece.text()}}:&nbsp;
span(ng-repeat='stat in ["str","con","per","int"]', ng-show='piece[stat]') {{piece[stat]}} {{stat.toUpperCase()}}&nbsp;
h4=env.t('attributes')
table.table.table-striped
tr(ng-repeat='(k,v) in {str:"Strength (STR)",int:"Intelligence (INT)",con:"Constitution (CON)",per:"Perception (PER)"}')
tr(ng-repeat='(k,v) in {str: env.t("strength") + " (STR)",int: env.t("intelligence") + " (INT)",con: env.t("constitution") + " (CON)",per:env.t("perception") + " (PER)"}')
td
span.hint(ng-if='k=="str"', popover-title=env.t('strength'), popover-placement='right', popover=env.t('strengthText'), popover-trigger='mouseenter', style='margin-right:3px')
strong {{v}}

View file

@ -35,7 +35,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
// Static Rewards
ul.items.rewards(ng-if='main && list.type=="reward" && user.flags.itemsEnabled')
li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.notes}}')
li.task.reward-item(ng-repeat='item in itemStore',popover-trigger='mouseenter', popover-placement='top', popover='{{item.notes()}}')
// right-hand side control buttons
.task-meta-controls
span.task-notes
@ -47,12 +47,12 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.shop_gold
// main content
span(bo-class='{"shop_{{item.key}} shop-sprite item-img": true}')
p.task-text {{item.text}}
p.task-text {{item.text()}}
// Events
ul.items.rewards(ng-if='main && list.type=="reward" && (user.items.special.snowball>0 || user.stats.buffs.snowball)')
li.task.reward-item(ng-if='user.items.special.snowball>0',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.snowball.notes}}')
li.task.reward-item(ng-if='user.items.special.snowball>0',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.snowball.notes()}}')
.task-meta-controls
span.task-notes
span.glyphicon.glyphicon-comment
@ -62,9 +62,9 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.reward-cost {{user.items.special.snowball}}
span.shop_snowball(style='display:inline-block;vertical-align:top;')
// main content
p.task-text {{Content.spells.special.snowball.text}}
p.task-text {{Content.spells.special.snowball.text()}}
li.task.reward-item(ng-if='user.stats.buffs.snowball',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.salt.notes}}')
li.task.reward-item(ng-if='user.stats.buffs.snowball',popover-trigger='mouseenter', popover-placement='top', popover='{{Content.spells.special.salt.notes()}}')
.task-meta-controls
span.task-notes
span.glyphicon.glyphicon-comment
@ -75,11 +75,11 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
span.shop_gold
// main content
span.shop_salt.shop-sprite.item-img
p.task-text {{Content.spells.special.salt.text}}
p.task-text {{Content.spells.special.salt.text()}}
// Spells
ul.items(ng-if='main && list.type=="reward" && user.stats.class && !user.preferences.disableClasses')
li.task.reward-item(ng-repeat='(k,spell) in Content.spells[user.stats.class]', ng-if='user.stats.lvl >= spell.lvl',popover-trigger='mouseenter', popover-placement='top', popover='{{spell.notes}}')
li.task.reward-item(ng-repeat='(k,spell) in Content.spells[user.stats.class]', ng-if='user.stats.lvl >= spell.lvl',popover-trigger='mouseenter', popover-placement='top', popover='{{spell.notes()}}')
.task-meta-controls
span.task-notes
span.glyphicon.glyphicon-comment
@ -91,7 +91,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template")
=env.t('mp')
// main content
span(ng-class='{"shop_{{spell.key}} shop-sprite item-img": true}')
p.task-text {{spell.text}}
p.task-text {{spell.text()}}
br

View file

@ -11,76 +11,81 @@ block content
.row#about-page(ng-controller='AboutCtrl')
.col-md-12#aboutPage
.marketing
h1 Improve Your Habits By Playing A Game
h1=env.t('marketing1Header')
.row
.col-md-6
a.gallery(href='/marketing/screenshot.png', title='Improve Your Habits By Playing A Game')
a.gallery(href='/marketing/screenshot.png', title=env.t('marketing1Header'))
img(src='/marketing/screenshot.png')
p.lead HabitRPG is a video game to help you improve real life habits. It "gamifies" your life by turning all your tasks (habits, dailies, and to-dos) into little monsters you have to conquor. The better you are at this, the more you progress in the game. If you slip up in life, your character starts backsliding in the game.
p.lead=env.t('marketing1Lead1')
.col-md-6
a.gallery(href='/marketing/gear.png', title='Get Sweet Gear')
a.gallery(href='/marketing/gear.png', title=env.t('marketing1Lead2Title'))
img(src='/marketing/gear.png')
p.lead <strong>Get Sweet Gear</strong>. Improve your habits to build up your avatar. Show off the sweet gear you've earned
p.lead!=env.t('marketing1Lead2')
a.gallery(href='/marketing/drops.png', title='Find Random Prizes')
a.gallery(href='/marketing/drops.png', title=env.t('marketing1Lead3Title'))
img(src='/marketing/drops.png',style='max-height:200px')
p.lead <strong>Find Random Prizes</strong>. For some, it's the gamble which motivates them, a system called "stochastic rewarding". HabitRPG accommodates all reinforcement styles: positive, negative, predictable, and random.
p.lead!=env.t('marketing1Lead3')
// TODO achievements
hr.clearfix
h1 Compete With Friends, Join Interest Groups
h1=env.t('marketing2Header')
.row
.col-md-6
a.gallery(href='/marketing/guild.png', title='Compete With Friends, Join Interest Groups')
a.gallery(href='/marketing/guild.png', title=env.t('marketing2Header'))
img(src='/marketing/guild.png')
p.lead While you can solo-play HabitRPG, the lights really turn on when you start collaborating, competing, and holding eachother accountable. The most effective part of any self-improvement program is social accountability, and what better an environment for accountability and competition than a video game?
p.lead=env.t('marketing2Lead1')
a.gallery(href='/bower_components/habitrpg-shared/img/sprites/spritesmith/quests/quest_vice3.png', title='Bosses')
a.gallery(href='/bower_components/habitrpg-shared/img/sprites/spritesmith/quests/quest_vice3.png', title=env.t('marketing2Lead2Title'))
img(src='/bower_components/habitrpg-shared/img/sprites/spritesmith/quests/quest_vice3.png')
p.lead <strong>Fight Bosses</strong>. What's a Role Playing Game without battles? Fight bosses with your party. Bosses are "super accountability mode" - a day you miss the gym is a day the boss hurts <em>everyone</em>.
p.lead!=env.t('marketing2Lead2')
.col-md-6
a.gallery(href='/marketing/challenge.png', title='Challenges')
a.gallery(href='/marketing/challenge.png', title=env.t('challenges'))
img(src='/marketing/challenge.png')
p.lead <strong>Challenges</strong> let you compete with friends and strangers. Whoever does the best at the end of a challenge wins special prizes.
p.lead!=env.t('marketing2Lead3')
hr.clearfix
h1 Apps
h1=env.t('marketing3Header')
.row
.col-md-6
a.gallery(href='/marketing/android_iphone.png', title='iPhone & Android')
a.gallery(href='/marketing/android_iphone.png', title=env.t('marketing3LeadTitle'))
img(src='/marketing/android_iphone.png',style='box-shadow:none;')
p.lead The <strong>iPhone & Android</strong> apps let you take care of business on the go. We realize that logging into the website to click buttons can be a drag.
p.lead!=env.t('marketing3Lead1')
.col-md-6
a.gallery(href='/marketing/integration.png', title='iPhone & Android')
a.gallery(href='/marketing/integration.png', title=env.t('marketing3LeadTitle'))
img(src='/marketing/integration.png')
p.lead Other <strong>3rd Party Tools</strong> tie HabitRPG into various aspects of your life. Our API provides easy integration for things like the <a href='https://chrome.google.com/webstore/detail/habitrpg/pidkmpibnnnhneohdgjclfdjpijggmjj?hl=en-US' target='_blank'>Chrome Extension</a>, for which you lose points when browsing unproductive websites, and gain points when on productive ones. <a href='http://habitrpg.wikia.com/wiki/App_and_Extension_Integrations' target='_blank'>See more here</a>
p.lead!=env.t('marketing3Lead2')
hr.clearfix
h1 Organizational Use
h1=env.t('marketing4Header')
.row
.col-md-6
h3 Gamification In Education
h3=env.t('marketing4Lead1Title')
img.pull-left(src='/marketing/education.png')
p.lead Education is one of the best sectors for gamification. We all know how glued to phones and games students are these days, harness that power! Pit your students against eachother in friendly competition. Reward good behavior with rare prizes. Watch their grades and behavior soar.
p.lead=env.t('marketing4Lead1')
.col-md-6
h3 Gamification In Health and Wellness
h3=env.t('marketing4Lead2Title')
img.pull-left(src='/marketing/wellness.png')
p.lead Health care costs are on the rise, and something's gotta give. Hundreds of programs are built to reduce costs and improve wellness. We believe HabitRPG can pave a substantial path towards healthy lifestyles.
p.lead=env.t('marketing4Lead2')
.row
.col-md-6.col-md-offset-3
h3 Gamify Everything
h3=env.t('marketing4Lead3Title')
img(src='/marketing/lefnire.png')
p.lead
| Want to gamify your life?&nbsp;
a.btn.btn-primary(ng-click='playButtonClick()') Play
| Interested in running a group in education, wellness, and more?&nbsp;
a.btn.btn-primary(href='/static/plans',target='_blank') Contact Us
| Want to learn more?&nbsp;
a.btn.btn-primary(href='/static/videos') Watch Videos
=env.t('marketing4Lead3-1')
|&nbsp;
a.btn.btn-primary(ng-click='playButtonClick()')=env.t('playButton')
|&nbsp;
=env.t('marketing4Lead3-2')
|&nbsp;
a.btn.btn-primary(href='/static/plans',target='_blank')=env.t('contactUs')
|&nbsp;
=env.t('marketing4Lead3-3')
|&nbsp;
a.btn.btn-primary(href='/static/videos')=env.t('watchVideos')

View file

@ -6,7 +6,7 @@ script(id='modals/login.html', type='text/ng-template')
a(href='/auth/facebook')
img(src='/bower_components/habitrpg-shared/img/facebook-login-register.jpeg', alt=env.t('loginFacebookAlt'))
//can we add in google auth? I like google auth
h3 Or
h3=env.t('or')
ul.nav.nav-tabs
li.active
a(data-toggle='tab',data-target='#login-tab')=env.t('login')