Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 581a40ffad | |||
| 61c78355bd | |||
| f82c93a9ec | |||
| e8c6340af8 | |||
| 1e66cc9193 | |||
| ef6d835391 | |||
| 51f7ce3280 | |||
| 2b7891e788 | |||
| c640a648b9 | |||
| 116f0446e5 | |||
| 065e8303df | |||
| 20fc233ad7 | |||
| adbe58a0da | |||
| b1b90d6dba | |||
| ef29247526 | |||
| 1c59e78ea9 | |||
| 18e4e11a95 | |||
| 82f34ed437 | |||
| 3b268287b1 | |||
| 0e253fbb06 | |||
| 043696c225 | |||
| 69c2b0378f | |||
| fcfc44fd13 | |||
| 76773d27c6 | |||
| b3e81177ec | |||
| 020726f6f8 | |||
| 9f9b0c73c3 | |||
| 2c03c114da | |||
| 8e9500fed2 | |||
| 377f849f9e | |||
| 8bbada540d | |||
| ee867a9d30 | |||
| 14a4f42b50 | |||
| 2cbb0a85a4 | |||
| 975e068fe2 | |||
| 5a5f1d6895 | |||
| 1db0cda6b1 | |||
| f987585cf1 | |||
| 0325ae9636 | |||
| 4c4fbfe790 | |||
| 6b59262e3e | |||
| 5630e8cc8e | |||
| 5268bbb8a9 | |||
| 34faf2fd50 | |||
| 896950dbf0 | |||
| 424c50f2a4 | |||
| f235a64d96 | |||
| 286abe334e | |||
| 81333a3074 | |||
| 3c31945524 | |||
| 103945f7c5 | |||
| b9d9a17aca | |||
| a0b6f576d2 | |||
| 30036963b1 | |||
| e1fe48bee4 | |||
| 77b19ffe97 | |||
| e9a5e084fc | |||
| 12250a93f1 | |||
| 7094e75dd8 | |||
| 74c93955f8 | |||
| e4b2ef6599 | |||
| a26c2bce23 | |||
| 6cd00897ed | |||
| 3c442318d8 | |||
| fd3f7e2b0e | |||
| 6f0bd0b913 | |||
| 5b7621ceb3 | |||
| b8b414cae5 | |||
| 783338f1f0 | |||
| fb6ca0c73b | |||
| 5792f54aa0 | |||
| 3c943c8f23 | |||
| 4b2fd60e47 | |||
| 75281ab638 | |||
| d46c9967fb | |||
| 4132a39b90 | |||
| 36cc229dd2 | |||
| ef0c11c2bd | |||
| 3a8312832c | |||
| df17753bb6 | |||
| c9b3c646eb | |||
| cd67877ef3 | |||
| 251e1d45af | |||
| 1a97d69edd | |||
| 7baa7427a0 | |||
| 270078a030 | |||
| bb2768071d | |||
| ec75de5a90 | |||
| b9b944ba29 | |||
| fa9553b371 | |||
| 8d5cbbe9be | |||
| 3a339a4a09 | |||
| 664cf5a47b | |||
| bcf304984d | |||
| 08d84ba691 | |||
| 4007c26801 | |||
| 344c20fd99 | |||
| f5c2c39f6a | |||
| bda3fb5f4d | |||
| eff8db0afd | |||
| 8cce38ede1 | |||
| ab0dae8df3 | |||
| 0824af05b7 | |||
| 28bf024990 | |||
| 8c59420d4e | |||
| 8a30ac0607 | |||
| e1984762b5 | |||
| 0360326f41 | |||
| f3f215abea | |||
| feb98a5ac7 | |||
| 2094a4d4b8 | |||
| 90532b0763 | |||
| 6fba71ea2c | |||
| 5755bfc952 | |||
| 08c6e8298c | |||
| 5a30b0cf1f | |||
| cf847cd1d8 | |||
| 5753d3e648 | |||
| 4718e5e5ea | |||
| 9a2dbace30 | |||
| ef07abfd28 | |||
| b28adc6b42 | |||
| c814eabb29 | |||
| 4dc19a8c60 | |||
| 89f6a9b07e | |||
| a23926f34f | |||
| 118198b594 | |||
| 97e80e2093 | |||
| 2588befceb | |||
| f09274225a | |||
| 03b66abe70 | |||
| 9d41fb0252 | |||
| 012fa2f8ef | |||
| 00d8d9d0cc | |||
| cc4772c75a | |||
| fabaaa6d92 | |||
| 854696728a | |||
| 314926cc06 | |||
| 68fa834946 | |||
| 09440d5adf | |||
| 1724bfc553 | |||
| 8304f99ecb | |||
| d2fcdf4493 | |||
| 649404ac6a | |||
| 774db2564f | |||
| 521a1e646d | |||
| 2619ac37d9 | |||
| 2ce9b319a0 | |||
| 477c23dd67 | |||
| 7af71d1457 | |||
| 3171550de2 | |||
| 6477801d3e | |||
| 14798ced82 | |||
| 34d37cefcc | |||
| bd21933cea |
@@ -1,18 +1,5 @@
|
||||
FROM node:10
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN cp config.json.example config.json
|
||||
RUN npm install
|
||||
|
||||
# Create Build dir
|
||||
RUN mkdir -p ./website/build
|
||||
|
||||
# Start Habitica
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
FROM node:10
|
||||
WORKDIR /code
|
||||
COPY package*.json /code/
|
||||
RUN npm install
|
||||
RUN npm install -g gulp-cli mocha
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||
"SITE_HTTP_AUTH_PASSWORD": "password",
|
||||
"SITE_HTTP_AUTH_USERNAME": "admin",
|
||||
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
||||
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
|
||||
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- BASE_URL=http://server:3000
|
||||
image: habitica
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
|
||||
- .:/code
|
||||
- /code/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
- mongo
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
image: habitica
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- '.:/usr/src/habitrpg'
|
||||
- .:/code
|
||||
- /code/node_modules
|
||||
mongo:
|
||||
image: mongo:3.4
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
|
||||
@@ -16,7 +16,7 @@ const IMG_DIST_PATH = 'website/client/assets/images/sprites/';
|
||||
const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
|
||||
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame|^eyewear_special_\w+HalfMoon/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20190530_halfmoon_glasses';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
'items.gear.owned.eyewear_special_blackHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_blueHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_greenHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_pinkHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_redHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_whiteHalfMoon': true,
|
||||
'items.gear.owned.eyewear_special_yellowHalfMoon': true,
|
||||
};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set}).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-05-01')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20190618_summer_splash_orcas';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set;
|
||||
|
||||
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME };
|
||||
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
|
||||
set = { migration: MIGRATION_NAME, 'items.pets.Orca-Base': 5 };
|
||||
} else {
|
||||
set = { migration: MIGRATION_NAME, 'items.mounts.Orca-Base': true };
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-05-18')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20190716_groups_fix';
|
||||
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
let backupUsers;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
let set = { migration: MIGRATION_NAME };
|
||||
let addToSet;
|
||||
|
||||
const monkPromise = new Promise((resolve, reject) => {
|
||||
backupUsers.findOne(
|
||||
{ _id: user._id },
|
||||
{ fields: { _id: 1, party: 1, guilds: 1 }},
|
||||
).then(foundUserInBackup => {
|
||||
resolve(foundUserInBackup);
|
||||
}).catch(e => {
|
||||
reject(e);
|
||||
})
|
||||
});
|
||||
let backupUser = await monkPromise;
|
||||
if (!backupUser) return;
|
||||
|
||||
if (!user.party._id) {
|
||||
set.party = backupUser.party;
|
||||
}
|
||||
addToSet = { guilds: { $each: backupUser.guilds }};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return User.update({ _id: user._id }, { $set: set, $addToSet: addToSet }).exec();
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
const query = {
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2019-07-15')},
|
||||
};
|
||||
|
||||
let backupDb = monk(CONNECTION_STRING);
|
||||
const backupDbPromise = new Promise((resolve, reject) => {
|
||||
backupDb.then(() => resolve()).catch((e) => reject(e));
|
||||
});
|
||||
|
||||
await backupDbPromise;
|
||||
console.log('Connected to backup db');
|
||||
backupUsers = backupDb.get('users', { castIds: false });
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
party: 1,
|
||||
guilds: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20190717_groups_fix_2';
|
||||
|
||||
import monk from 'monk';
|
||||
import nconf from 'nconf';
|
||||
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { sendTxn as sendTxnEmail } from '../../../website/server/libs/email';
|
||||
import shared from '../../../website/common';
|
||||
|
||||
const questScrolls = shared.content.quests;
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
let backupUsers;
|
||||
|
||||
async function updateGroup (group) {
|
||||
count++;
|
||||
|
||||
if (group && group.quest && group.quest.leader) {
|
||||
const quest = questScrolls[group.quest.key];
|
||||
const leader = await User.findOne({_id: group.quest.leader}).exec();
|
||||
|
||||
if (!leader) return;
|
||||
|
||||
await User.update({ _id: leader._id }, {
|
||||
$set: {migration: MIGRATION_NAME},
|
||||
$inc: {
|
||||
balance: 1,
|
||||
[`items.quests.${group.quest.key}`]: 1,
|
||||
},
|
||||
}).exec();
|
||||
|
||||
// unsubscribe from all is already checked by sendTxnEmail
|
||||
if (leader.preferences && leader.preferences.emailNotifications && leader.preferences.emailNotifications.majorUpdates !== false) {
|
||||
sendTxnEmail(leader, 'groups-outage');
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${group._id}`);
|
||||
}
|
||||
|
||||
module.exports = async function processUsers () {
|
||||
const query = {
|
||||
type: 'party'
|
||||
};
|
||||
|
||||
let backupDb = monk(CONNECTION_STRING);
|
||||
const backupDbPromise = new Promise((resolve, reject) => {
|
||||
backupDb.then(() => resolve()).catch((e) => reject(e));
|
||||
});
|
||||
|
||||
await backupDbPromise;
|
||||
console.log('Connected to backup db');
|
||||
const backupGroups = backupDb.get('groups', { castIds: false });
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const groupsPromise = new Promise((resolve, reject) => {
|
||||
backupGroups
|
||||
.find(query, {
|
||||
limit: 250,
|
||||
sort: {_id: 1}
|
||||
})
|
||||
.then(foundGroupInBackup => {
|
||||
resolve(foundGroupInBackup);
|
||||
}).catch(e => {
|
||||
reject(e);
|
||||
})
|
||||
});
|
||||
|
||||
const groups = await groupsPromise;
|
||||
|
||||
if (groups.length === 0) {
|
||||
console.warn('All appropriate groups found and modified.');
|
||||
console.warn(`\n${count} groups processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: groups[groups.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(groups.map(updateGroup)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -17,7 +17,7 @@ function setUpServer () {
|
||||
setUpServer();
|
||||
|
||||
// Replace this with your migration
|
||||
const processUsers = require('./users/mystery-items.js');
|
||||
const processUsers = require('');
|
||||
processUsers()
|
||||
.then(function success () {
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = 'mystery_items_201904';
|
||||
const MYSTERY_ITEMS = ['armor_mystery_201904', 'head_mystery_201904'];
|
||||
const MIGRATION_NAME = 'mystery_items_201906';
|
||||
const MYSTERY_ITEMS = ['headAccessory_mystery_201906', 'armor_mystery_201906'];
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
import { model as UserNotification } from '../../website/server/models/userNotification';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.96.1",
|
||||
"version": "4.104.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/trace-agent": "^3.6.0",
|
||||
@@ -14,7 +14,7 @@
|
||||
"apn": "^2.2.0",
|
||||
"autoprefixer": "^9.4.0",
|
||||
"aws-sdk": "^2.432.0",
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.19.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
@@ -48,7 +48,7 @@
|
||||
"got": "^9.0.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-imagemin": "^5.0.3",
|
||||
"gulp-imagemin": "^6.0.0",
|
||||
"gulp-nodemon": "^2.4.1",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.0",
|
||||
@@ -58,9 +58,9 @@
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": ">=3.0.0",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"merge-stream": "^1.0.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.22.1",
|
||||
"moment-recur": "^1.0.7",
|
||||
@@ -71,7 +71,7 @@
|
||||
"node-sass": "^4.9.0",
|
||||
"nodemailer": "^6.0.0",
|
||||
"ora": "^3.2.0",
|
||||
"pageres": "^4.1.1",
|
||||
"pageres": "^5.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-google-oauth20": "1.0.0",
|
||||
@@ -98,7 +98,7 @@
|
||||
"url-loader": "^1.0.0",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^10.5.0",
|
||||
"validator": "^11.0.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vue": "^2.6.10",
|
||||
"vue-loader": "^14.2.2",
|
||||
@@ -152,7 +152,7 @@
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^2.4.1",
|
||||
"chromedriver": "^2.40.0",
|
||||
"chromedriver": "^73.0.0",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^3.0.3",
|
||||
"cross-spawn": "^6.0.5",
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('auth middleware', () => {
|
||||
describe('auth with headers', () => {
|
||||
it('allows to specify a list of user field that we do not want to load', (done) => {
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
|
||||
userFieldsToExclude: ['items'],
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
@@ -27,11 +27,34 @@ describe('auth middleware', () => {
|
||||
|
||||
const userToJSON = res.locals.user.toJSON();
|
||||
expect(userToJSON.items).to.not.exist;
|
||||
expect(userToJSON.flags).to.not.exist;
|
||||
expect(userToJSON.auth.timestamps).to.not.exist;
|
||||
expect(userToJSON.auth).to.exist;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('makes sure some fields are always included', (done) => {
|
||||
const authWithHeaders = authWithHeadersFactory({
|
||||
userFieldsToExclude: [
|
||||
'items', 'auth.timestamps',
|
||||
'preferences', 'notifications', '_id', 'flags', 'auth', // these are always loaded
|
||||
],
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = user._id;
|
||||
req.headers['x-api-key'] = user.apiToken;
|
||||
|
||||
authWithHeaders(req, res, (err) => {
|
||||
if (err) return done(err);
|
||||
|
||||
const userToJSON = res.locals.user.toJSON();
|
||||
expect(userToJSON.items).to.not.exist;
|
||||
expect(userToJSON.auth.timestamps).to.exist;
|
||||
expect(userToJSON.auth).to.exist;
|
||||
expect(userToJSON.notifications).to.exist;
|
||||
expect(userToJSON.preferences).to.exist;
|
||||
expect(userToJSON._id).to.exist;
|
||||
expect(userToJSON.flags).to.exist;
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import validator from 'validator';
|
||||
import { sleep } from '../../../helpers/api-unit.helper';
|
||||
import { sleep, translationCheck } from '../../../helpers/api-unit.helper';
|
||||
import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
@@ -271,7 +271,16 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member attacks Wailing Whale for 5.0 damage. Wailing Whale attacks party for 7.5 damage.`',
|
||||
info: {
|
||||
bossDamage: '7.5',
|
||||
quest: 'whale',
|
||||
type: 'boss_damage',
|
||||
user: 'Participating Member',
|
||||
userDamage: '5.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('applies damage only to participating members of party', async () => {
|
||||
@@ -344,7 +353,10 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledTwice;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`You defeated Wailing Whale! Questing party members receive the rewards of victory.`',
|
||||
info: { quest: 'whale', type: 'boss_defeated' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls finishQuest when boss has <= 0 hp', async () => {
|
||||
@@ -387,7 +399,10 @@ describe('Group Model', () => {
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: quest.boss.rage.effect('en'),
|
||||
info: { quest: 'trex_undead', type: 'boss_rage' },
|
||||
});
|
||||
expect(party.quest.progress.hp).to.eql(383.5);
|
||||
expect(party.quest.progress.rage).to.eql(0);
|
||||
});
|
||||
@@ -437,7 +452,10 @@ describe('Group Model', () => {
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en'));
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: quest.boss.rage.effect('en'),
|
||||
info: { quest: 'lostMasterclasser4', type: 'boss_rage' },
|
||||
});
|
||||
expect(party.quest.progress.rage).to.eql(0);
|
||||
|
||||
let drainedUser = await User.findById(participatingMember._id);
|
||||
@@ -488,7 +506,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 5 Bars of Soap.`',
|
||||
info: {
|
||||
items: { soapBars: 5 },
|
||||
quest: 'atom1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a chat message if no progress is made', async () => {
|
||||
@@ -499,7 +525,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 0 Bars of Soap.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Bars of Soap.`',
|
||||
info: {
|
||||
items: { soapBars: 0 },
|
||||
quest: 'atom1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends a chat message if no progress is made on quest with multiple items', async () => {
|
||||
@@ -516,9 +550,15 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Blue Fins/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/0 Fire Coral/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`Participating Member found 0 Fire Coral, 0 Blue Fins.`',
|
||||
info: {
|
||||
items: { blueFins: 0, fireCoral: 0 },
|
||||
quest: 'dilatoryDistress1',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('handles collection quests with multiple items', async () => {
|
||||
@@ -535,8 +575,14 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/`Participating Member found/).and(sinon.match(/\d* (Tracks|Broken Twigs)/)),
|
||||
info: {
|
||||
quest: 'evilsanta2',
|
||||
type: 'user_found_items',
|
||||
user: 'Participating Member',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sends message about victory', async () => {
|
||||
@@ -547,7 +593,10 @@ describe('Group Model', () => {
|
||||
party = await Group.findOne({_id: party._id});
|
||||
|
||||
expect(Group.prototype.sendChat).to.be.calledTwice;
|
||||
expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`');
|
||||
expect(Group.prototype.sendChat).to.be.calledWith({
|
||||
message: '`All items found! Party has received their rewards.`',
|
||||
info: { type: 'all_items_found' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls finishQuest when all items are found', async () => {
|
||||
@@ -718,6 +767,258 @@ describe('Group Model', () => {
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateSystemMessages', () => {
|
||||
it('translate quest_start', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_damage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_damage',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
userDamage: 15.3,
|
||||
bossDamage: 3.7,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_dont_attack', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_dont_attack',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
userDamage: 15.3,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_rage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_rage',
|
||||
quest: 'lostMasterclasser3',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate boss_defeated', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'boss_defeated',
|
||||
quest: 'lostMasterclasser3',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate user_found_items', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'user_found_items',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'lostMasterclasser1',
|
||||
items: {
|
||||
ancientTome: 3,
|
||||
forbiddenTome: 2,
|
||||
hiddenTome: 1,
|
||||
},
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate all_items_found', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'all_items_found',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate spell_cast_party', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'spell_cast_party',
|
||||
user: questLeader.profile.name,
|
||||
class: 'wizard',
|
||||
spell: 'earth',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate spell_cast_user', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'spell_cast_user',
|
||||
user: questLeader.profile.name,
|
||||
class: 'special',
|
||||
spell: 'snowball',
|
||||
target: participatingMember.profile.name,
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate quest_cancel', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_cancel',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate quest_abort', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'quest_abort',
|
||||
user: questLeader.profile.name,
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_quest_completed', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_quest_completed',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_rage_tired', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_rage_tired',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_rage', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_rage',
|
||||
quest: 'dysheartener',
|
||||
scene: 'market',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate tavern_boss_desperation', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'tavern_boss_desperation',
|
||||
quest: 'stressbeast',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
|
||||
it('translate claim_task', async () => {
|
||||
questLeader.preferences.language = 'en';
|
||||
party.chat = [{
|
||||
info: {
|
||||
type: 'claim_task',
|
||||
user: questLeader.profile.name,
|
||||
task: 'Feed the pet',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
translationCheck(toJSON.chat[0].text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toJSONCleanChat', () => {
|
||||
it('shows messages with 1 flag to non-admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 1,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('shows messages with >= 2 flag to admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
const admin = new User({'contributor.admin': true});
|
||||
let toJSON = await Group.toJSONCleanChat(party, admin);
|
||||
expect(toJSON.chat.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('doesn\'t show flagged messages to non-admins', async () => {
|
||||
party.chat = [{
|
||||
flagCount: 3,
|
||||
info: {
|
||||
type: 'quest_start',
|
||||
quest: 'basilist',
|
||||
},
|
||||
}];
|
||||
let toJSON = await Group.toJSONCleanChat(party, questLeader);
|
||||
expect(toJSON.chat.length).to.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Instance Methods', () => {
|
||||
@@ -1007,20 +1308,22 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('formats message', () => {
|
||||
const chatMessage = party.sendChat('a new message', {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
contributor: {
|
||||
toObject () {
|
||||
return 'contributor object';
|
||||
const chatMessage = party.sendChat({
|
||||
message: 'a new message', user: {
|
||||
_id: 'user-id',
|
||||
profile: { name: 'user name' },
|
||||
contributor: {
|
||||
toObject () {
|
||||
return 'contributor object';
|
||||
},
|
||||
},
|
||||
},
|
||||
backer: {
|
||||
toObject () {
|
||||
return 'backer object';
|
||||
backer: {
|
||||
toObject () {
|
||||
return 'backer object';
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
);
|
||||
|
||||
const chat = chatMessage;
|
||||
|
||||
@@ -1037,7 +1340,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('formats message as system if no user is passed in', () => {
|
||||
const chat = party.sendChat('a system message');
|
||||
const chat = party.sendChat({message: 'a system message'});
|
||||
|
||||
expect(chat.text).to.eql('a system message');
|
||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||
@@ -1052,7 +1355,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('updates users about new messages in party', () => {
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1066,7 +1369,7 @@ describe('Group Model', () => {
|
||||
type: 'guild',
|
||||
});
|
||||
|
||||
group.sendChat('message');
|
||||
group.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1076,7 +1379,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('does not send update to user that sent the message', () => {
|
||||
party.sendChat('message', {_id: 'user-id', profile: { name: 'user' }});
|
||||
party.sendChat({message: 'message', user: {_id: 'user-id', profile: { name: 'user' }}});
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
@@ -1088,7 +1391,7 @@ describe('Group Model', () => {
|
||||
it('skips sending new message notification for guilds with > 5000 members', () => {
|
||||
party.memberCount = 5001;
|
||||
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.not.be.called;
|
||||
});
|
||||
@@ -1096,7 +1399,7 @@ describe('Group Model', () => {
|
||||
it('skips sending messages to the tavern', () => {
|
||||
party._id = TAVERN_ID;
|
||||
|
||||
party.sendChat('message');
|
||||
party.sendChat({message: 'message'});
|
||||
|
||||
expect(User.update).to.not.be.called;
|
||||
});
|
||||
@@ -1431,7 +1734,7 @@ describe('Group Model', () => {
|
||||
let quest;
|
||||
|
||||
beforeEach(() => {
|
||||
quest = questScrolls.whale;
|
||||
quest = questScrolls.armadillo;
|
||||
party.quest.key = quest.key;
|
||||
party.quest.active = false;
|
||||
party.quest.leader = questLeader._id;
|
||||
@@ -1579,6 +1882,36 @@ describe('Group Model', () => {
|
||||
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
|
||||
});
|
||||
|
||||
it('gives out other pet-related quest achievements', async () => {
|
||||
quest = questScrolls.rock;
|
||||
party.quest.key = quest.key;
|
||||
|
||||
questLeader.achievements.quests = {
|
||||
mayhemMistiflying1: 1,
|
||||
yarn: 1,
|
||||
mayhemMistiflying2: 1,
|
||||
egg: 1,
|
||||
mayhemMistiflying3: 1,
|
||||
slime: 2,
|
||||
};
|
||||
await questLeader.save();
|
||||
await party.finishQuest(quest);
|
||||
|
||||
let [
|
||||
updatedLeader,
|
||||
updatedParticipatingMember,
|
||||
updatedSleepingParticipatingMember,
|
||||
] = await Promise.all([
|
||||
User.findById(questLeader._id).exec(),
|
||||
User.findById(participatingMember._id).exec(),
|
||||
User.findById(sleepingParticipatingMember._id).exec(),
|
||||
]);
|
||||
|
||||
expect(updatedLeader.achievements.mindOverMatter).to.eql(true);
|
||||
expect(updatedParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
|
||||
expect(updatedSleepingParticipatingMember.achievements.mindOverMatter).to.not.eql(true);
|
||||
});
|
||||
|
||||
it('gives xp and gold', async () => {
|
||||
await party.finishQuest(quest);
|
||||
|
||||
@@ -1715,13 +2048,13 @@ describe('Group Model', () => {
|
||||
questLeader = await User.findById(questLeader._id);
|
||||
participatingMember = await User.findById(participatingMember._id);
|
||||
|
||||
expect(questLeader.party.quest.completed).to.eql('whale');
|
||||
expect(questLeader.party.quest.completed).to.eql('armadillo');
|
||||
expect(questLeader.party.quest.progress.up).to.eql(10);
|
||||
expect(questLeader.party.quest.progress.down).to.eql(8);
|
||||
expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
|
||||
expect(questLeader.party.quest.RSVPNeeded).to.eql(false);
|
||||
|
||||
expect(participatingMember.party.quest.completed).to.eql('whale');
|
||||
expect(participatingMember.party.quest.completed).to.eql('armadillo');
|
||||
expect(participatingMember.party.quest.progress.up).to.eql(10);
|
||||
expect(participatingMember.party.quest.progress.down).to.eql(8);
|
||||
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
|
||||
@@ -1928,7 +2261,7 @@ describe('Group Model', () => {
|
||||
|
||||
await guild.save();
|
||||
|
||||
const groupMessage = guild.sendChat('Test message.');
|
||||
const groupMessage = guild.sendChat({message: 'Test message.'});
|
||||
await groupMessage.save();
|
||||
|
||||
await sleep();
|
||||
|
||||
@@ -171,7 +171,7 @@ describe('GET challenges/user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return not return challenges in user groups if we send member true param', async () => {
|
||||
it('should not return challenges in user groups if we send member true param', async () => {
|
||||
let challenges = await member.get(`/challenges/user?member=${true}`);
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
@@ -214,6 +214,28 @@ describe('GET challenges/user', () => {
|
||||
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
|
||||
expect(foundChallenge).to.not.exist;
|
||||
});
|
||||
|
||||
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'TestPrivateGuild',
|
||||
summary: 'summary for TestPrivateGuild',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
});
|
||||
|
||||
let privateChallenge = await generateChallenge(groupLeader, group, {categories: [{
|
||||
name: 'academics',
|
||||
slug: 'academics',
|
||||
}]});
|
||||
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
|
||||
|
||||
let challenges = await nonMember.get('/challenges/user?categories=academics&owned=not_owned');
|
||||
|
||||
let foundChallenge = _.find(challenges, { _id: privateChallenge._id });
|
||||
expect(foundChallenge).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
context('official challenge is present', () => {
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('POST /group', () => {
|
||||
).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotCreatePublicGuildWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,7 +100,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -262,7 +262,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -436,7 +436,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('cannotInviteWhenMuted'),
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -127,7 +127,13 @@ describe('POST /groups/:groupId/quests/abort', () => {
|
||||
members: {},
|
||||
});
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/aborted the party quest Wail of the Whale.`/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/aborted the party quest Wail of the Whale.`/),
|
||||
info: {
|
||||
quest: 'whale',
|
||||
type: 'quest_abort',
|
||||
},
|
||||
});
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
@@ -141,7 +141,14 @@ describe('POST /groups/:groupId/quests/cancel', () => {
|
||||
members: {},
|
||||
});
|
||||
expect(Group.prototype.sendChat).to.be.calledOnce;
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch(/cancelled the party quest Wail of the Whale.`/);
|
||||
expect(Group.prototype.sendChat).to.be.calledWithMatch({
|
||||
message: sinon.match(/cancelled the party quest Wail of the Whale.`/),
|
||||
info: {
|
||||
quest: 'whale',
|
||||
type: 'quest_cancel',
|
||||
user: sinon.match.any,
|
||||
},
|
||||
});
|
||||
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
@@ -83,12 +83,12 @@ context('avatar.vue', () => {
|
||||
expect(vm.paddingTop).to.equal('28px');
|
||||
});
|
||||
|
||||
it('is 24.5px if user has a pet', () => {
|
||||
it('is 24px if user has a pet', () => {
|
||||
vm.member.items = {
|
||||
currentPet: { name: 'Foo' },
|
||||
};
|
||||
|
||||
expect(vm.paddingTop).to.equal('24.5px');
|
||||
expect(vm.paddingTop).to.equal('24px');
|
||||
});
|
||||
|
||||
it('is 0px if user has a mount', () => {
|
||||
@@ -297,4 +297,4 @@ context('avatar.vue', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,6 +80,13 @@ describe('shared.ops.buy', () => {
|
||||
headAccessory_special_redHeadband: true,
|
||||
headAccessory_special_whiteHeadband: true,
|
||||
headAccessory_special_yellowHeadband: true,
|
||||
eyewear_special_blackHalfMoon: true,
|
||||
eyewear_special_blueHalfMoon: true,
|
||||
eyewear_special_greenHalfMoon: true,
|
||||
eyewear_special_pinkHalfMoon: true,
|
||||
eyewear_special_redHalfMoon: true,
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -75,6 +75,13 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
headAccessory_special_redHeadband: true,
|
||||
headAccessory_special_whiteHeadband: true,
|
||||
headAccessory_special_yellowHeadband: true,
|
||||
eyewear_special_blackHalfMoon: true,
|
||||
eyewear_special_blueHalfMoon: true,
|
||||
eyewear_special_greenHalfMoon: true,
|
||||
eyewear_special_pinkHalfMoon: true,
|
||||
eyewear_special_redHalfMoon: true,
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
@@ -169,6 +169,24 @@ describe('shared.ops.feed', () => {
|
||||
expect(user.items.pets['Wolf-Base']).to.equal(7);
|
||||
});
|
||||
|
||||
it('awards All Your Base achievement', () => {
|
||||
user.items.pets['Wolf-Spooky'] = 5;
|
||||
user.items.food.Milk = 2;
|
||||
user.items.mounts = {
|
||||
'Wolf-Base': true,
|
||||
'TigerCub-Base': true,
|
||||
'PandaCub-Base': true,
|
||||
'LionCub-Base': true,
|
||||
'Fox-Base': true,
|
||||
'FlyingPig-Base': true,
|
||||
'Dragon-Base': true,
|
||||
'Cactus-Base': true,
|
||||
'BearCub-Base': true,
|
||||
};
|
||||
feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}});
|
||||
expect(user.achievements.allYourBase).to.eql(true);
|
||||
});
|
||||
|
||||
it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => {
|
||||
user.items.pets['Wolf-Base'] = 49;
|
||||
user.items.food.Milk = 2;
|
||||
|
||||
@@ -159,6 +159,24 @@ describe('shared.ops.hatch', () => {
|
||||
expect(user.items.eggs).to.eql({Wolf: 0});
|
||||
expect(user.items.hatchingPotions).to.eql({Base: 0});
|
||||
});
|
||||
|
||||
it('awards Back to Basics achievement', () => {
|
||||
user.items.pets = {
|
||||
'Wolf-Base': 5,
|
||||
'TigerCub-Base': 5,
|
||||
'PandaCub-Base': 10,
|
||||
'LionCub-Base': 5,
|
||||
'Fox-Base': 5,
|
||||
'FlyingPig-Base': 5,
|
||||
'Dragon-Base': 5,
|
||||
'Cactus-Base': 15,
|
||||
'BearCub-Base': 5,
|
||||
};
|
||||
user.items.eggs = {Wolf: 1};
|
||||
user.items.hatchingPotions = {Spooky: 1};
|
||||
hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}});
|
||||
expect(user.achievements.backToBasics).to.eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('shared.ops.rebirth', () => {
|
||||
let [, message] = rebirth(user);
|
||||
|
||||
expect(message).to.equal(i18n.t('rebirthComplete'));
|
||||
expect(user.flags.lastFreeRebirth).to.exist;
|
||||
});
|
||||
|
||||
it('rebirths a user with not enough gems but more than max level', () => {
|
||||
@@ -60,6 +61,16 @@ describe('shared.ops.rebirth', () => {
|
||||
expect(message).to.equal(i18n.t('rebirthComplete'));
|
||||
});
|
||||
|
||||
it('rebirths a user using gems if over max level but rebirthed recently', () => {
|
||||
user.stats.lvl = MAX_LEVEL + 1;
|
||||
user.flags.lastFreeRebirth = new Date();
|
||||
|
||||
let [, message] = rebirth(user);
|
||||
|
||||
expect(message).to.equal(i18n.t('rebirthComplete'));
|
||||
expect(user.balance).to.equal(0);
|
||||
});
|
||||
|
||||
it('resets user\'s tasks values except for rewards to 0', () => {
|
||||
tasks[0].value = 1;
|
||||
tasks[1].value = 1;
|
||||
|
||||
@@ -8,6 +8,7 @@ import mongo from './mongo'; // eslint-disable-line
|
||||
import moment from 'moment';
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
import * as Tasks from '../../website/server/models/task';
|
||||
export { translationCheck } from './translate';
|
||||
|
||||
afterEach((done) => {
|
||||
sandbox.restore();
|
||||
|
||||
@@ -16,3 +16,9 @@ export function translate (key, variables, language) {
|
||||
|
||||
return translatedString;
|
||||
}
|
||||
|
||||
export function translationCheck (translatedString) {
|
||||
expect(translatedString).to.not.be.empty;
|
||||
expect(translatedString).to.not.eql(STRING_ERROR_MSG);
|
||||
expect(translatedString).to.not.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
}
|
||||
|
||||
@@ -656,5 +656,6 @@ export default {
|
||||
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-23.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-24.css"></style>
|
||||
<style src="assets/css/sprites/spritesmith-main-25.css"></style>
|
||||
<style src="assets/css/sprites.css"></style>
|
||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
||||
|
||||
@@ -1,66 +1,78 @@
|
||||
.promo_armoire_backgrounds_201905 {
|
||||
.promo_armoire_backgrounds_201907 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -786px;
|
||||
background-position: -313px -524px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_bronze_quest {
|
||||
.promo_glass_watery_potions {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -499px 0px;
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
}
|
||||
.promo_feathered_friends_bundle {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -638px;
|
||||
background-position: -854px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_floral_sunshine_potions {
|
||||
.promo_mystery_201906 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -638px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_201904 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -860px -223px;
|
||||
background-position: -854px -296px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_orcas {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -854px -444px;
|
||||
width: 219px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_seafoam {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -376px -337px;
|
||||
width: 425px;
|
||||
height: 148px;
|
||||
}
|
||||
.promo_seasonal_shop {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1074px -444px;
|
||||
width: 162px;
|
||||
height: 132px;
|
||||
}
|
||||
.promo_splashy_pals_bundle {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -854px -148px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_splashy_skins {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -337px;
|
||||
width: 375px;
|
||||
height: 186px;
|
||||
}
|
||||
.customize-option.promo_splashy_skins {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -25px -352px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_summer_splash_2019 {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -445px 0px;
|
||||
width: 408px;
|
||||
height: 186px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -1068px -371px;
|
||||
background-position: -1137px -296px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_gold {
|
||||
.scene_casting_spells {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 498px;
|
||||
height: 360px;
|
||||
}
|
||||
.scene_languages {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -328px -361px;
|
||||
width: 297px;
|
||||
height: 261px;
|
||||
}
|
||||
.scene_rewards {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -860px -371px;
|
||||
width: 207px;
|
||||
height: 180px;
|
||||
}
|
||||
.scene_spells {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -860px 0px;
|
||||
background-position: 0px -524px;
|
||||
width: 312px;
|
||||
height: 222px;
|
||||
}
|
||||
.scene_yesterdailies_repeatables {
|
||||
.scene_hat_guild {
|
||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -361px;
|
||||
width: 327px;
|
||||
height: 276px;
|
||||
background-position: 0px 0px;
|
||||
width: 444px;
|
||||
height: 336px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 561 KiB After Width: | Height: | Size: 491 KiB |
|
Before Width: | Height: | Size: 580 KiB After Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 309 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 112 KiB |
@@ -35,6 +35,7 @@ $maroon-50: #C92B2B;
|
||||
$maroon-100: #DE3F3F;
|
||||
$maroon-500: #F19595;
|
||||
|
||||
$yellow-5: #EE9109;
|
||||
$yellow-10: #FFA623;
|
||||
$yellow-50: #FFB445;
|
||||
$yellow-100: #FFBE5D;
|
||||
@@ -60,9 +61,6 @@ $green-50: #3FDAA2;
|
||||
$green-100: #5AEAB2;
|
||||
$green-500: #A6FFDF;
|
||||
|
||||
$orange-10: #ee9109;
|
||||
$orange-50: #bf7d1a;
|
||||
|
||||
$suggested-item-color: #D5C8FF;
|
||||
|
||||
$healer-color: #cf8229;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// possible values are: normal, fall, habitoween, thanksgiving, winter, nye, birthday, valentines, spring, summer
|
||||
// more to be added on future seasons
|
||||
|
||||
$npc_market_flavor: 'normal';
|
||||
$npc_quests_flavor: 'normal';
|
||||
$npc_seasonal_flavor: 'normal';
|
||||
$npc_timetravelers_flavor: 'normal';
|
||||
$npc_tavern_flavor: 'normal';
|
||||
$npc_market_flavor: 'summer';
|
||||
$npc_quests_flavor: 'summer';
|
||||
$npc_seasonal_flavor: 'summer';
|
||||
$npc_timetravelers_flavor: 'summer';
|
||||
$npc_tavern_flavor: 'summer';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="9" viewBox="0 0 14 9">
|
||||
<path fill="none" fill-rule="evenodd" stroke="#BDA8FF" stroke-width="2.5" d="M13 1L7 7 1 1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 187 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#FFF" d="M8 0a8 8 0 1 0 0 16A8 8 0 0 0 8 0m0 2c3.308 0 6 2.692 6 6s-2.692 6-6 6-6-2.692-6-6 2.692-6 6-6"/>
|
||||
<path stroke="#FFF" stroke-linecap="round" stroke-width="2" d="M8 5v3.031L10 10"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#E1E0E3" fill-rule="evenodd" d="M8 0c2.173 0 2.445.01 3.298.048.852.04 1.433.174 1.942.372.526.205.973.478 1.418.922.444.445.717.892.922 1.418.198.509.333 1.09.372 1.942C15.99 5.555 16 5.827 16 8s-.01 2.445-.048 3.298c-.04.852-.174 1.433-.372 1.942a3.924 3.924 0 0 1-.922 1.418 3.924 3.924 0 0 1-1.418.922c-.509.198-1.09.333-1.942.372-.853.04-1.125.048-3.298.048s-2.445-.009-3.298-.048c-.852-.04-1.433-.174-1.942-.372a3.924 3.924 0 0 1-1.418-.922A3.924 3.924 0 0 1 .42 13.24c-.198-.509-.333-1.09-.372-1.942C.01 10.445 0 10.173 0 8s.01-2.445.048-3.298C.088 3.85.222 3.269.42 2.76c.205-.526.478-.973.922-1.418A3.924 3.924 0 0 1 2.76.42C3.269.222 3.85.087 4.702.048 5.555.01 5.827 0 8 0zm0 3.892a4.108 4.108 0 1 0 0 8.216 4.108 4.108 0 0 0 0-8.216zm5.23-.162a.96.96 0 1 0-1.92 0 .96.96 0 0 0 1.92 0zM8 10.667a2.666 2.666 0 1 1 0-5.333 2.666 2.666 0 0 1 0 5.333z"/>
|
||||
<path fill-rule="evenodd" d="M8 0c2.173 0 2.445.01 3.298.048.852.04 1.433.174 1.942.372.526.205.973.478 1.418.922.444.445.717.892.922 1.418.198.509.333 1.09.372 1.942C15.99 5.555 16 5.827 16 8s-.01 2.445-.048 3.298c-.04.852-.174 1.433-.372 1.942a3.924 3.924 0 0 1-.922 1.418 3.924 3.924 0 0 1-1.418.922c-.509.198-1.09.333-1.942.372-.853.04-1.125.048-3.298.048s-2.445-.009-3.298-.048c-.852-.04-1.433-.174-1.942-.372a3.924 3.924 0 0 1-1.418-.922A3.924 3.924 0 0 1 .42 13.24c-.198-.509-.333-1.09-.372-1.942C.01 10.445 0 10.173 0 8s.01-2.445.048-3.298C.088 3.85.222 3.269.42 2.76c.205-.526.478-.973.922-1.418A3.924 3.924 0 0 1 2.76.42C3.269.222 3.85.087 4.702.048 5.555.01 5.827 0 8 0zm0 3.892a4.108 4.108 0 1 0 0 8.216 4.108 4.108 0 0 0 0-8.216zm5.23-.162a.96.96 0 1 0-1.92 0 .96.96 0 0 0 1.92 0zM8 10.667a2.666 2.666 0 1 1 0-5.333 2.666 2.666 0 0 1 0 5.333z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 969 B After Width: | Height: | Size: 954 B |
@@ -164,30 +164,31 @@ export default {
|
||||
classGear (heroClass) {
|
||||
if (heroClass === 'rogue') {
|
||||
return {
|
||||
armor: 'armor_rogue_5',
|
||||
head: 'head_rogue_5',
|
||||
shield: 'shield_rogue_6',
|
||||
weapon: 'weapon_rogue_6',
|
||||
armor: 'armor_special_summer2019Rogue',
|
||||
head: 'head_special_summer2019Rogue',
|
||||
shield: 'shield_special_summer2019Rogue',
|
||||
weapon: 'weapon_special_summer2019Rogue',
|
||||
};
|
||||
} else if (heroClass === 'wizard') {
|
||||
return {
|
||||
armor: 'armor_wizard_5',
|
||||
head: 'head_wizard_5',
|
||||
weapon: 'weapon_wizard_6',
|
||||
armor: 'armor_special_summer2019Mage',
|
||||
head: 'head_special_summer2019Mage',
|
||||
shield: 'shield_special_summer2019Mage',
|
||||
weapon: 'weapon_special_summer2019Mage',
|
||||
};
|
||||
} else if (heroClass === 'healer') {
|
||||
return {
|
||||
armor: 'armor_healer_5',
|
||||
head: 'head_healer_5',
|
||||
shield: 'shield_healer_5',
|
||||
weapon: 'weapon_healer_6',
|
||||
armor: 'armor_special_summer2019Healer',
|
||||
head: 'head_special_summer2019Healer',
|
||||
shield: 'shield_special_summer2019Healer',
|
||||
weapon: 'weapon_special_summer2019Healer',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
armor: 'armor_warrior_5',
|
||||
head: 'head_warrior_5',
|
||||
shield: 'shield_warrior_5',
|
||||
weapon: 'weapon_warrior_6',
|
||||
armor: 'armor_special_summer2019Warrior',
|
||||
head: 'head_special_summer2019Warrior',
|
||||
shield: 'shield_special_summer2019Warrior',
|
||||
weapon: 'weapon_special_summer2019Warrior',
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template lang="pug">
|
||||
b-modal#generic-achievement(:title='data.message', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p(v-html='data.modalText')
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
props: ['data'],
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'generic-achievement');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#just-add-water(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementJustAddWaterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementJustAddWater')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#lost-masterclasser(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementLostMasterclasserModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementLostMasterclasser')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template lang="pug">
|
||||
b-modal#mind-over-matter(:title='title', size='md', :hide-footer='true')
|
||||
.modal-body
|
||||
.col-12
|
||||
achievement-avatar.avatar
|
||||
.col-6.offset-3.text-center
|
||||
p {{ $t('achievementMindOverMatterModalText') }}
|
||||
button.btn.btn-primary(@click='close()') {{ $t('huzzah') }}
|
||||
achievement-footer
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import {mapState} from 'client/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementMindOverMatter')}`,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -139,7 +139,7 @@ export default {
|
||||
let val = '28px';
|
||||
|
||||
if (!this.avatarOnly) {
|
||||
if (this.member.items.currentPet) val = '24.5px';
|
||||
if (this.member.items.currentPet) val = '24px';
|
||||
if (this.member.items.currentMount) val = '0px';
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`skin.${set.keys.join(",skin.")}`)') {{ $t('purchaseAll') }}
|
||||
#hair.section.customize-section(v-if='activeTopPage === "hair"')
|
||||
.row.col-12.sub-menu.text-center
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color"}')
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("color")', :class='{active: activeSubPage === "color", "offset-2": !editing}')
|
||||
strong(v-once) {{$t('color')}}
|
||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("bangs")', :class='{active: activeSubPage === "bangs"}')
|
||||
strong(v-once) {{$t('bangs')}}
|
||||
@@ -139,6 +139,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
span 5
|
||||
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.base.${baseHair4Keys.join(",hair.base.")}`)') {{ $t('purchaseAll') }}
|
||||
.col-12.customize-options
|
||||
.head_0.option(v-if="!editing", @click='set({"preferences.hair.base": 0})', :class="[{ active: user.preferences.hair.base === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
|
||||
.option(v-for='option in baseHair1',
|
||||
:class='{active: user.preferences.hair.base === option}')
|
||||
.base.sprite.customize-option(:class="`hair_base_${option}_${user.preferences.hair.color}`", @click='set({"preferences.hair.base": option})')
|
||||
@@ -377,8 +378,8 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
username-form(@usernameConfirmed='modalPage += 1', :avatarIntro='true')
|
||||
.small.text-center(v-html="$t('usernameTOSRequirements')")
|
||||
|
||||
.section.container.footer
|
||||
.row(v-if='!editing && !(modalPage === 1)')
|
||||
.section.container.footer(v-if='!editing && !(modalPage === 1)')
|
||||
.row
|
||||
.col-3.offset-1.text-center
|
||||
div(v-if='modalPage > 1', @click='prev()')
|
||||
.prev-arrow
|
||||
@@ -406,6 +407,10 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#avatar-modal .modal-dialog {
|
||||
margin-top: 7rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1125,7 +1130,10 @@ export default {
|
||||
return options;
|
||||
},
|
||||
eyewear () {
|
||||
let keys = ['blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame'];
|
||||
let keys = [
|
||||
'blackTopFrame', 'blueTopFrame', 'greenTopFrame', 'pinkTopFrame', 'redTopFrame', 'whiteTopFrame', 'yellowTopFrame',
|
||||
'blackHalfMoon', 'blueHalfMoon', 'greenHalfMoon', 'pinkHalfMoon', 'redHalfMoon', 'whiteHalfMoon', 'yellowHalfMoon',
|
||||
];
|
||||
let options = keys.map(key => {
|
||||
let newKey = `eyewear_special_${key}`;
|
||||
let option = {};
|
||||
|
||||
@@ -25,7 +25,7 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
|
||||
div.guild-bank(v-if='displayGemBank', v-once) {{$t('guildBank')}}
|
||||
.row
|
||||
category-tags.col-md-12(:categories="guild.categories", :owner="isOwner", v-once)
|
||||
span.recommend-text(v-if='showSuggested(guild._id)') Suggested because you’re new to Habitica.
|
||||
span.recommend-text(v-if='showSuggested(guild._id)') {{$t('suggestedGroup')}}
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -175,8 +175,9 @@ export default {
|
||||
methods: {
|
||||
showSuggested (guildId) {
|
||||
let habiticaHelpingGuildId = '5481ccf3-5d2d-48a9-a871-70a7380cee5a';
|
||||
let createdAfterRedesign = moment(this.user.auth.timestamps.created).isAfter('2017-08-01');
|
||||
return guildId === habiticaHelpingGuildId && createdAfterRedesign;
|
||||
let sixtyDaysAgoFromNow = moment().subtract(60, 'days');
|
||||
let isUserNew = moment(this.user.auth.timestamps.created).isAfter(sixtyDaysAgoFromNow);
|
||||
return guildId === habiticaHelpingGuildId && isUserNew;
|
||||
},
|
||||
async join () {
|
||||
// @TODO: This needs to be in the notifications where users will now accept invites
|
||||
|
||||
@@ -35,7 +35,7 @@ sidebar-section(:title="$t('questDetailsTitle')")
|
||||
.grey-progress-bar
|
||||
.collect-progress-bar(:style="{width: (group.quest.progress.collect[key] / value.count) * 100 + '%'}")
|
||||
strong {{group.quest.progress.collect[key]}} / {{value.count}}
|
||||
div.text-right {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
|
||||
div.text-right(v-if='userIsOnQuest') {{parseFloat(user.party.quest.progress.collectedItems) || 0}} items found
|
||||
.boss-info(v-if='questData.boss')
|
||||
.row
|
||||
.col-6
|
||||
|
||||
@@ -6,49 +6,61 @@ div
|
||||
report-flag-modal
|
||||
send-gems-modal
|
||||
b-navbar.topbar.navbar-inverse.static-top(toggleable="lg", type="dark", :class="navbarZIndexClass")
|
||||
b-navbar-brand.brand
|
||||
b-navbar-brand.brand(aria-label="Habitica")
|
||||
.logo.svg-icon.d-none.d-xl-block(v-html="icons.logo")
|
||||
.svg-icon.gryphon.d-xs-block.d-xl-none
|
||||
b-navbar-toggle(target='menu_collapse').menu-toggle
|
||||
.quick-menu.mobile-only.form-inline
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')", :aria-label="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
b-collapse#menu_collapse.collapse.navbar-collapse
|
||||
b-collapse#menu_collapse(v-model="menuIsOpen").collapse.navbar-collapse
|
||||
b-navbar-nav.menu-list
|
||||
b-nav-item.topbar-item(tag="li", :to="{name: 'tasks'}", exact) {{ $t('tasks') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/inventory')}")
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path === '/'}" tag="li", :to="{name: 'tasks'}", exact) {{ $t('tasks') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/inventory'), 'down': $route.path.startsWith('/inventory') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'items'}") {{ $t('inventory') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/shop')}")
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/shop'), 'down': $route.path.startsWith('/shop') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'market'}") {{ $t('shops') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }}
|
||||
b-nav-item.topbar-item(tag="li", :to="{name: 'party'}", v-if='this.user.party._id') {{ $t('party') }}
|
||||
b-nav-item.topbar-item(@click='openPartyModal()', v-if='!this.user.party._id') {{ $t('party') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/guilds')}")
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path.startsWith('/party')}" tag="li", :to="{name: 'party'}", v-if='this.user.party._id') {{ $t('party') }}
|
||||
b-nav-item.topbar-item(:class="{'active': $route.path.startsWith('/party')}" @click='openPartyModal()', v-if='!this.user.party._id') {{ $t('party') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/groups'), 'down': $route.path.startsWith('/groups') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'tavern'}") {{ $t('guilds') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/group-plans')}")
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/group-plans'), 'down': $route.path.startsWith('/group-plans') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(v-if="groupPlans.length > 0", @click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'groupPlan'}") {{ $t('group') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/challenges')}")
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/challenges'), 'down': $route.path.startsWith('/challenges') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'myChallenges'}") {{ $t('challenges') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }}
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }}
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/help')}")
|
||||
li.topbar-item(:class="{'active': $route.path.startsWith('/help'), 'down': $route.path.startsWith('/help') && this.isDesktop()}").droppable
|
||||
.chevron.rotate(@click='dropdownMobile($event)')
|
||||
.chevron-icon-down(v-html="icons.chevronDown", v-once)
|
||||
router-link.nav-link(:to="{name: 'faq'}") {{ $t('help') }}
|
||||
.topbar-dropdown
|
||||
router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }}
|
||||
@@ -64,13 +76,13 @@ div
|
||||
.top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.top-menu-icon.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
a.top-menu-icon.svg-icon.gem(:aria-label="$t('gems')", href="#buy-gems" v-html="icons.gem", @click.prevent='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems}}
|
||||
.item-with-icon.gold
|
||||
.top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
.top-menu-icon.svg-icon(:aria-label="$t('gold')", v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
.form-inline.desktop-only
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
a.item-with-icon(@click="sync", @keyup.enter="sync", role="link", :aria-label="$t('sync')", tabindex="0", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.top-menu-icon.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
@@ -81,6 +93,10 @@ div
|
||||
@import '~client/assets/scss/utils.scss';
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.chevron {
|
||||
display: none
|
||||
}
|
||||
|
||||
.gryphon {
|
||||
background-image: url('~assets/images/melior@3x.png');
|
||||
width: 30px;
|
||||
@@ -96,6 +112,10 @@ div
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 992px) {
|
||||
.chevron {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -111,6 +131,10 @@ div
|
||||
padding-top: 5px;
|
||||
height: 56px;
|
||||
|
||||
&:hover {
|
||||
background: $purple-200;
|
||||
}
|
||||
|
||||
&.active:not(:hover) {
|
||||
box-shadow: 0px -4px 0px $purple-300 inset;
|
||||
}
|
||||
@@ -144,12 +168,40 @@ div
|
||||
order: 1;
|
||||
text-align: center;
|
||||
|
||||
.topbar-dropdown {
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
|
||||
.topbar-dropdown-item {
|
||||
background: #432874;
|
||||
border-bottom: #6133b4 solid 1px;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 20%;
|
||||
height: 42px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chevron-icon-down {
|
||||
width: 14px;
|
||||
top: 11px;
|
||||
right: 12px;
|
||||
position: absolute;
|
||||
display: block;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.down .rotate .chevron-icon-down {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: #6133b4;
|
||||
}
|
||||
@@ -223,20 +275,25 @@ div
|
||||
font-weight: bold;
|
||||
transition: none;
|
||||
|
||||
.topbar-dropdown {
|
||||
display: none; // Display is set to block on hover.
|
||||
.topbar-dropdown {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
>a {
|
||||
padding: .8em 1em !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.down {
|
||||
color: $white !important;
|
||||
background: $purple-200;
|
||||
|
||||
.topbar-dropdown {
|
||||
display: block; // Open drop-down on hover.
|
||||
margin-top: 0; // Remove gap between navbar and drop-down.
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
@@ -290,6 +347,7 @@ div
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:focus /deep/ .top-menu-icon.svg-icon,
|
||||
&:hover /deep/ .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
@@ -343,6 +401,7 @@ import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import syncIcon from 'assets/svg/sync.svg';
|
||||
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||
import chevronDownIcon from 'assets/svg/chevron-down.svg';
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
|
||||
import creatorIntro from '../creatorIntro';
|
||||
@@ -369,12 +428,14 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isUserDropdownOpen: false,
|
||||
menuIsOpen: false,
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
gold: goldIcon,
|
||||
hourglasses: svgHourglasses,
|
||||
sync: syncIcon,
|
||||
logo,
|
||||
chevronDown: chevronDownIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
@@ -397,6 +458,13 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
this.getUserGroupPlans();
|
||||
Array.from(document.getElementById('menu_collapse').getElementsByTagName('a')).forEach(link => {
|
||||
link.addEventListener('click', this.closeMenu);
|
||||
});
|
||||
Array.from(document.getElementsByClassName('topbar-item')).forEach(link => {
|
||||
link.addEventListener('mouseenter', this.dropdownDesktop);
|
||||
link.addEventListener('mouseleave', this.dropdownDesktop);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
modForm () {
|
||||
@@ -423,7 +491,40 @@ export default {
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
|
||||
},
|
||||
dropdownDesktop (hover) {
|
||||
if (this.isDesktop() && hover.target.classList.contains('droppable')) {
|
||||
this.dropdown(hover.target);
|
||||
}
|
||||
},
|
||||
dropdownMobile (click) {
|
||||
this.dropdown(click.currentTarget.parentElement);
|
||||
},
|
||||
dropdown (element) {
|
||||
let droppedElement = document.getElementsByClassName('down')[0];
|
||||
if (droppedElement && droppedElement !== element) {
|
||||
droppedElement.classList.remove('down');
|
||||
droppedElement.lastChild.style.maxHeight = 0;
|
||||
}
|
||||
|
||||
element.classList.toggle('down');
|
||||
element.lastChild.style.maxHeight = element.classList.contains('down') ? `${element.lastChild.scrollHeight}px` : 0;
|
||||
},
|
||||
closeMenu () {
|
||||
if (this.isMobile()) {
|
||||
this.menuIsOpen = false;
|
||||
|
||||
Array.from(document.getElementsByClassName('droppable')).forEach(droppableElement => {
|
||||
droppableElement.classList.remove('down');
|
||||
droppableElement.lastChild.style.maxHeight = 0;
|
||||
});
|
||||
}
|
||||
},
|
||||
isMobile () {
|
||||
return document.documentElement.clientWidth < 992;
|
||||
},
|
||||
isDesktop () {
|
||||
return !this.isMobile();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementJustAddWater')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementLostMasterclasser')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template lang="pug">
|
||||
base-notification(
|
||||
:can-remove="canRemove",
|
||||
:notification="notification",
|
||||
:read-after-click="true",
|
||||
@click="action"
|
||||
)
|
||||
div(slot="content", v-html="achievementString")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementMindOverMatter')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||