mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
Compare commits
156 Commits
phillip/co
...
fiz/update
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a69f98906 | ||
|
|
5d1d70b72e | ||
|
|
b9764ddfcf | ||
|
|
fdbae638bb | ||
|
|
949809a994 | ||
|
|
ef59ecb3b1 | ||
|
|
4250fbc53f | ||
|
|
1d48439528 | ||
|
|
08ff95d015 | ||
|
|
ec91b674f1 | ||
|
|
ae4130b108 | ||
|
|
ad0614282e | ||
|
|
45e1b97ebd | ||
|
|
c1ab9cb6ca | ||
|
|
5cf07d75f5 | ||
|
|
375cafc781 | ||
|
|
0c0dc20dcc | ||
|
|
ffa73698a5 | ||
|
|
f1ac0d5038 | ||
|
|
c0508a2f22 | ||
|
|
5a7704aed7 | ||
|
|
53aa960afd | ||
|
|
d16c9bc67a | ||
|
|
00a468bde9 | ||
|
|
c71528a478 | ||
|
|
2feadd6125 | ||
|
|
efe0b3cd9e | ||
|
|
96731da380 | ||
|
|
0c5dd5d8b5 | ||
|
|
2f943a22e6 | ||
|
|
666184d7e4 | ||
|
|
17d22dda3f | ||
|
|
d1a18c121d | ||
|
|
836d7f3991 | ||
|
|
ace9c3c46a | ||
|
|
068640311e | ||
|
|
f26d2a59ae | ||
|
|
03c7e9172e | ||
|
|
6fdc072ec3 | ||
|
|
e68661c04b | ||
|
|
4f567592ea | ||
|
|
63c9b7a894 | ||
|
|
eaec39188e | ||
|
|
ba6940eb81 | ||
|
|
f8a3e4d673 | ||
|
|
2727da6f6c | ||
|
|
fa97852e38 | ||
|
|
2c7da25a25 | ||
|
|
338c633cdb | ||
|
|
b957d2a3b0 | ||
|
|
9a072e3e76 | ||
|
|
b2efb8286b | ||
|
|
823b339d27 | ||
|
|
fe98d9485d | ||
|
|
407e1bb560 | ||
|
|
98a6535dc3 | ||
|
|
9948e8ee44 | ||
|
|
bce07ec357 | ||
|
|
836807aa1e | ||
|
|
ebbcbef6d5 | ||
|
|
ccc6c9867f | ||
|
|
20d31ed8c8 | ||
|
|
39ff6cbe05 | ||
|
|
1bf2efa885 | ||
|
|
4b45a6389c | ||
|
|
5ba7d2395e | ||
|
|
972f23e235 | ||
|
|
9f599b0c8e | ||
|
|
b937c2df0b | ||
|
|
9c4396027a | ||
|
|
2bab20d032 | ||
|
|
cb2ee670e3 | ||
|
|
b65d23d535 | ||
|
|
007cdf0ca2 | ||
|
|
1e4799bac6 | ||
|
|
47222445ad | ||
|
|
126b382da1 | ||
|
|
ec78831a81 | ||
|
|
9bfb2afd9c | ||
|
|
389124b83f | ||
|
|
eb25330296 | ||
|
|
29892ff5e3 | ||
|
|
99a31b322a | ||
|
|
1884c6c751 | ||
|
|
9456477953 | ||
|
|
e3512a2bdd | ||
|
|
6ce3f84458 | ||
|
|
484c3cbac8 | ||
|
|
c199beaf8c | ||
|
|
553aa01c25 | ||
|
|
8d1b10e458 | ||
|
|
0eaee9b1e4 | ||
|
|
41bbc475ab | ||
|
|
d6e03c765e | ||
|
|
dd6503d5ef | ||
|
|
36e5f39d7c | ||
|
|
d48e4a664f | ||
|
|
661b30e807 | ||
|
|
026e819271 | ||
|
|
1fab19acf4 | ||
|
|
5743fb86b0 | ||
|
|
5443bf2459 | ||
|
|
c0d5566417 | ||
|
|
ded71b46c5 | ||
|
|
9693ad321c | ||
|
|
dd3679f329 | ||
|
|
f3029953dc | ||
|
|
01881b2fd8 | ||
|
|
11a22d0f5d | ||
|
|
5f9bf07045 | ||
|
|
719c03e2f5 | ||
|
|
379afa9554 | ||
|
|
dbc23e89b8 | ||
|
|
0c6e254742 | ||
|
|
8327e69bdd | ||
|
|
2d953f4f59 | ||
|
|
7118d63949 | ||
|
|
20af8d038e | ||
|
|
3d9dfbb5e1 | ||
|
|
ae0b966f45 | ||
|
|
cef8a34c06 | ||
|
|
6432823eec | ||
|
|
563b780d85 | ||
|
|
aa9b1b2cac | ||
|
|
401e541b86 | ||
|
|
c13bed3bad | ||
|
|
b3c4817fb4 | ||
|
|
7c9c45ac5f | ||
|
|
95142e3684 | ||
|
|
dc1cce6ddb | ||
|
|
43cf77f33c | ||
|
|
93780d7056 | ||
|
|
2ad17d408e | ||
|
|
b0f7567367 | ||
|
|
3f2b1d3f79 | ||
|
|
29eb8ca10b | ||
|
|
8c71ca12b8 | ||
|
|
72a753626f | ||
|
|
35ebb12bf2 | ||
|
|
1ff418f62d | ||
|
|
e1aa437ea5 | ||
|
|
2a4239bf3c | ||
|
|
399563435b | ||
|
|
59f7e25c85 | ||
|
|
ad845dff43 | ||
|
|
fd1eb2d900 | ||
|
|
26cb6df9d9 | ||
|
|
b0aafb079a | ||
|
|
58f0837c50 | ||
|
|
a6378b3d43 | ||
|
|
ddbf95da92 | ||
|
|
4d31e0286b | ||
|
|
7a74825121 | ||
|
|
be0e8779d5 | ||
|
|
fffbe17bcc | ||
|
|
ca4ee8b513 |
@@ -7,5 +7,14 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'prefer-regex-literals': 'warn',
|
'prefer-regex-literals': 'warn',
|
||||||
'import/no-extraneous-dependencies': 'off',
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'require-await': 'error',
|
||||||
},
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['migrations/**', 'gulp/**'], // Or *.test.js
|
||||||
|
rules: {
|
||||||
|
'require-await': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -1,6 +1,13 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'phillip/**'
|
||||||
|
- 'sabrecat/**'
|
||||||
|
- 'kalista/**'
|
||||||
|
- 'natalie/**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
|
||||||
|
|
||||||
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.
|
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
|
||||||
|
|||||||
@@ -93,5 +93,6 @@
|
|||||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||||
"TIME_TRAVEL_ENABLED": "false",
|
"TIME_TRAVEL_ENABLED": "false",
|
||||||
"DEBUG_ENABLED": "false",
|
"DEBUG_ENABLED": "false",
|
||||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
|
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||||
|
"SLOW_REQUEST_THRESHOLD": 1000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,15 @@ function filterFile (file) {
|
|||||||
if (file.relative.indexOf('icon_background') === 0) {
|
if (file.relative.indexOf('icon_background') === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (file.relative.indexOf('notif_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.relative.indexOf('quest_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.relative.indexOf('inventory_quest_') === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import gulp from 'gulp';
|
|
||||||
import nodemon from 'gulp-nodemon';
|
|
||||||
|
|
||||||
import pkg from '../package.json';
|
|
||||||
|
|
||||||
gulp.task('nodemon', done => {
|
|
||||||
nodemon({
|
|
||||||
script: pkg.main,
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
@@ -49,12 +49,6 @@ function integrationTestCommand (testDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Test task definitions */
|
/* Test task definitions */
|
||||||
gulp.task('test:nodemon', gulp.series(done => {
|
|
||||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
|
||||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
|
||||||
done();
|
|
||||||
}, 'nodemon'));
|
|
||||||
|
|
||||||
gulp.task('test:prepare:mongo', cb => {
|
gulp.task('test:prepare:mongo', cb => {
|
||||||
const mongooseOptions = getDefaultConnectionOptions();
|
const mongooseOptions = getDefaultConnectionOptions();
|
||||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
|
|||||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
require('./gulp/gulp-console'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
|
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-start'); // eslint-disable-line global-require
|
|
||||||
require('./gulp/gulp-tests'); // eslint-disable-line global-require
|
require('./gulp/gulp-tests'); // eslint-disable-line global-require
|
||||||
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
|
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
|
||||||
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
|
||||||
|
|||||||
Submodule habitica-images updated: dfb04339a4...992d838120
@@ -37,7 +37,7 @@ let consoleStamp = require('console-stamp');
|
|||||||
consoleStamp(console);
|
consoleStamp(console);
|
||||||
|
|
||||||
// Initialize configuration
|
// Initialize configuration
|
||||||
require('../../website/server/libs/api-v3/setupNconf')();
|
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||||
|
|
||||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ let moment = require('moment');
|
|||||||
consoleStamp(console);
|
consoleStamp(console);
|
||||||
|
|
||||||
// Initialize configuration
|
// Initialize configuration
|
||||||
require('../../website/server/libs/api-v3/setupNconf')();
|
require('../../website/server/libs/api-v3/setupNconf').default();
|
||||||
|
|
||||||
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
let MONGODB_OLD = nconf.get('MONGODB_OLD');
|
||||||
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
let MONGODB_NEW = nconf.get('MONGODB_NEW');
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
|
|||||||
function setUpServer () {
|
function setUpServer () {
|
||||||
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
|
||||||
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
|
||||||
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
|
const setupNconf = require('../website/server/libs/setupNconf').default; // eslint-disable-line global-require
|
||||||
|
|
||||||
setupNconf();
|
setupNconf();
|
||||||
|
|
||||||
|
|||||||
917
package-lock.json
generated
917
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||||
"version": "5.32.5",
|
"version": "5.38.1",
|
||||||
"main": "./website/server/index.js",
|
"main": "./website/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.22.10",
|
"@babel/core": "^7.22.10",
|
||||||
@@ -38,7 +38,6 @@
|
|||||||
"gulp-babel": "^8.0.0",
|
"gulp-babel": "^8.0.0",
|
||||||
"gulp-filter": "^7.0.0",
|
"gulp-filter": "^7.0.0",
|
||||||
"gulp-imagemin": "^7.1.0",
|
"gulp-imagemin": "^7.1.0",
|
||||||
"gulp-nodemon": "^2.5.0",
|
|
||||||
"gulp.spritesmith": "^6.13.0",
|
"gulp.spritesmith": "^6.13.0",
|
||||||
"habitica-markdown": "^3.0.0",
|
"habitica-markdown": "^3.0.0",
|
||||||
"helmet": "^4.6.0",
|
"helmet": "^4.6.0",
|
||||||
@@ -50,12 +49,11 @@
|
|||||||
"merge-stream": "^2.0.0",
|
"merge-stream": "^2.0.0",
|
||||||
"method-override": "^3.0.0",
|
"method-override": "^3.0.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-recur": "^1.0.7",
|
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||||
"mongoose": "^7.8.3",
|
"mongoose": "^8.9.5",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"node-gcm": "^1.0.5",
|
"node-gcm": "^1.0.5",
|
||||||
"nodemon": "^2.0.20",
|
|
||||||
"on-headers": "^1.0.2",
|
"on-headers": "^1.0.2",
|
||||||
"passport": "^0.5.3",
|
"passport": "^0.5.3",
|
||||||
"passport-facebook": "^3.0.0",
|
"passport-facebook": "^3.0.0",
|
||||||
@@ -100,15 +98,14 @@
|
|||||||
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
|
||||||
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
|
||||||
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
|
||||||
"test:nodemon": "gulp test:nodemon",
|
|
||||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||||
"sprites": "gulp sprites:compile",
|
"sprites": "gulp sprites:compile",
|
||||||
"client:dev": "cd website/client && npm run serve",
|
"client:dev": "cd website/client && npm run serve",
|
||||||
"client:build": "cd website/client && npm run build",
|
"client:build": "cd website/client && npm run build",
|
||||||
"client:unit": "cd website/client && npm run test:unit",
|
"client:unit": "cd website/client && npm run test:unit",
|
||||||
"start": "gulp nodemon",
|
"start": "node --watch ./website/server/index.js",
|
||||||
"start:simple": "node ./website/server/index.js",
|
"start:simple": "node ./website/server/index.js",
|
||||||
"debug": "gulp nodemon --inspect",
|
"debug": "node --watch --inspect ./website/server/index.js",
|
||||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||||
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||||
@@ -116,7 +113,7 @@
|
|||||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.8.2",
|
||||||
"chai": "^4.3.7",
|
"chai": "^4.3.7",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"chai-moment": "^0.1.0",
|
"chai-moment": "^0.1.0",
|
||||||
|
|||||||
@@ -71,15 +71,14 @@ async function deleteHabiticaData (user, email) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function processEmailAddress (email) {
|
async function processEmailAddress (email) {
|
||||||
const emailRegex = new RegExp(`^${email}$`, 'i');
|
|
||||||
const localUsers = await User.find(
|
const localUsers = await User.find(
|
||||||
{ 'auth.local.email': emailRegex },
|
{ 'auth.local.email': email },
|
||||||
{ _id: 1, apiToken: 1, auth: 1 },
|
{ _id: 1, apiToken: 1, auth: 1 },
|
||||||
).exec();
|
).exec();
|
||||||
|
|
||||||
const socialUsers = await User.find(
|
const socialUsers = await User.find(
|
||||||
{
|
{
|
||||||
'auth.local.email': { $not: emailRegex },
|
'auth.local.email': { $ne: email },
|
||||||
$or: [
|
$or: [
|
||||||
{ 'auth.facebook.emails.value': email },
|
{ 'auth.facebook.emails.value': email },
|
||||||
{ 'auth.google.emails.value': email },
|
{ 'auth.google.emails.value': email },
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ const TASK_VALUE_CHANGE_FACTOR = 0.9747;
|
|||||||
const MIN_TASK_VALUE = -47.27;
|
const MIN_TASK_VALUE = -47.27;
|
||||||
|
|
||||||
async function updateTeamTasks (team) {
|
async function updateTeamTasks (team) {
|
||||||
|
if (team.purchased.plan.dateTerminated) {
|
||||||
|
const dateTerminated = new Date(team.purchased.plan.dateTerminated);
|
||||||
|
if (dateTerminated < new Date()) {
|
||||||
|
team.purchased.plan.customerId = undefined;
|
||||||
|
team.markModified('purchased.plan');
|
||||||
|
return team.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toSave = [];
|
const toSave = [];
|
||||||
|
|
||||||
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
|
||||||
|
|
||||||
if (!teamLeader) { // why would this happen?
|
if (!teamLeader) { // why would this happen?
|
||||||
@@ -93,12 +103,7 @@ async function updateTeamTasks (team) {
|
|||||||
export default async function processTeamsCron () {
|
export default async function processTeamsCron () {
|
||||||
const activeTeams = await Group.find({
|
const activeTeams = await Group.find({
|
||||||
'purchased.plan.customerId': { $exists: true },
|
'purchased.plan.customerId': { $exists: true },
|
||||||
$or: [
|
}, { cron: 1, leader: 1, purchased: 1 }).exec();
|
||||||
{ 'purchased.plan.dateTerminated': { $exists: false } },
|
|
||||||
{ 'purchased.plan.dateTerminated': null },
|
|
||||||
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
|
|
||||||
],
|
|
||||||
}).exec();
|
|
||||||
|
|
||||||
const cronPromises = activeTeams.map(updateTeamTasks);
|
const cronPromises = activeTeams.map(updateTeamTasks);
|
||||||
return Promise.all(cronPromises);
|
return Promise.all(cronPromises);
|
||||||
|
|||||||
@@ -2,13 +2,22 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import requireAgain from 'require-again';
|
import requireAgain from 'require-again';
|
||||||
import { recoverCron, cron } from '../../../../website/server/libs/cron';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import {
|
||||||
|
generateRes,
|
||||||
|
generateReq,
|
||||||
|
generateTodo,
|
||||||
|
generateDaily,
|
||||||
|
} from '../../../helpers/api-unit.helper';
|
||||||
|
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||||
import { model as User } from '../../../../website/server/models/user';
|
import { model as User } from '../../../../website/server/models/user';
|
||||||
import * as Tasks from '../../../../website/server/models/task';
|
import * as Tasks from '../../../../website/server/models/task';
|
||||||
import common from '../../../../website/common';
|
import common from '../../../../website/common';
|
||||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||||
|
import { model as Group } from '../../../../website/server/models/group';
|
||||||
|
|
||||||
// const scoreTask = common.ops.scoreTask;
|
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||||
|
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||||
|
|
||||||
const pathToCronLib = '../../../../website/server/libs/cron';
|
const pathToCronLib = '../../../../website/server/libs/cron';
|
||||||
|
|
||||||
@@ -1200,7 +1209,7 @@ describe('cron', async () => {
|
|||||||
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
|
||||||
daysMissed = 1;
|
daysMissed = 1;
|
||||||
tasksByType.dailys[0].completed = true;
|
tasksByType.dailys[0].completed = true;
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
tasksByType.dailys[0].isDue = true;
|
||||||
|
|
||||||
await cron({
|
await cron({
|
||||||
user, tasksByType, daysMissed, analytics,
|
user, tasksByType, daysMissed, analytics,
|
||||||
@@ -1212,7 +1221,7 @@ describe('cron', async () => {
|
|||||||
it('does not increment perfect day achievement if no due dailies', async () => {
|
it('does not increment perfect day achievement if no due dailies', async () => {
|
||||||
daysMissed = 1;
|
daysMissed = 1;
|
||||||
tasksByType.dailys[0].completed = true;
|
tasksByType.dailys[0].completed = true;
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
tasksByType.dailys[0].isDue = false;
|
||||||
|
|
||||||
await cron({
|
await cron({
|
||||||
user, tasksByType, daysMissed, analytics,
|
user, tasksByType, daysMissed, analytics,
|
||||||
@@ -1224,7 +1233,7 @@ describe('cron', async () => {
|
|||||||
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
|
||||||
daysMissed = 1;
|
daysMissed = 1;
|
||||||
tasksByType.dailys[0].completed = true;
|
tasksByType.dailys[0].completed = true;
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
tasksByType.dailys[0].isDue = true;
|
||||||
|
|
||||||
const previousBuffs = user.stats.buffs.toObject();
|
const previousBuffs = user.stats.buffs.toObject();
|
||||||
|
|
||||||
@@ -1242,7 +1251,7 @@ describe('cron', async () => {
|
|||||||
user.preferences.sleep = true;
|
user.preferences.sleep = true;
|
||||||
daysMissed = 1;
|
daysMissed = 1;
|
||||||
tasksByType.dailys[0].completed = true;
|
tasksByType.dailys[0].completed = true;
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
tasksByType.dailys[0].isDue = true;
|
||||||
|
|
||||||
const previousBuffs = user.stats.buffs.toObject();
|
const previousBuffs = user.stats.buffs.toObject();
|
||||||
|
|
||||||
@@ -1259,7 +1268,7 @@ describe('cron', async () => {
|
|||||||
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
|
||||||
daysMissed = 1;
|
daysMissed = 1;
|
||||||
tasksByType.dailys[0].completed = true;
|
tasksByType.dailys[0].completed = true;
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
|
tasksByType.dailys[0].isDue = false;
|
||||||
|
|
||||||
user.stats.buffs = {
|
user.stats.buffs = {
|
||||||
str: 1,
|
str: 1,
|
||||||
@@ -1488,78 +1497,6 @@ describe('cron', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('notifications', async () => {
|
|
||||||
it('adds a user notification', async () => {
|
|
||||||
const mpBefore = user.stats.mp;
|
|
||||||
tasksByType.dailys[0].completed = true;
|
|
||||||
|
|
||||||
const statsComputedRes = common.statsComputed(user);
|
|
||||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
||||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
|
||||||
|
|
||||||
daysMissed = 1;
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
||||||
|
|
||||||
await cron({
|
|
||||||
user, tasksByType, daysMissed, analytics,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.notifications.length).to.be.greaterThan(0);
|
|
||||||
expect(user.notifications[1].type).to.equal('CRON');
|
|
||||||
expect(user.notifications[1].data).to.eql({
|
|
||||||
hp: user.stats.hp - hpBefore,
|
|
||||||
mp: user.stats.mp - mpBefore,
|
|
||||||
});
|
|
||||||
|
|
||||||
common.statsComputed.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('condenses multiple notifications into one', async () => {
|
|
||||||
const mpBefore1 = user.stats.mp;
|
|
||||||
tasksByType.dailys[0].completed = true;
|
|
||||||
|
|
||||||
const statsComputedRes = common.statsComputed(user);
|
|
||||||
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
|
|
||||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
|
||||||
|
|
||||||
daysMissed = 1;
|
|
||||||
const hpBefore1 = user.stats.hp;
|
|
||||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
|
||||||
|
|
||||||
await cron({
|
|
||||||
user, tasksByType, daysMissed, analytics,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.notifications.length).to.be.greaterThan(0);
|
|
||||||
expect(user.notifications[1].type).to.equal('CRON');
|
|
||||||
expect(user.notifications[1].data).to.eql({
|
|
||||||
hp: user.stats.hp - hpBefore1,
|
|
||||||
mp: user.stats.mp - mpBefore1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const notifsBefore2 = user.notifications.length;
|
|
||||||
const hpBefore2 = user.stats.hp;
|
|
||||||
const mpBefore2 = user.stats.mp;
|
|
||||||
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
|
|
||||||
await cron({
|
|
||||||
user, tasksByType, daysMissed, analytics,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.notifications.length - notifsBefore2).to.equal(0);
|
|
||||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
|
||||||
expect(user.notifications[1].type).to.equal('CRON');
|
|
||||||
expect(user.notifications[1].data).to.eql({
|
|
||||||
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
|
|
||||||
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
|
|
||||||
});
|
|
||||||
expect(user.notifications[0].type).to.not.equal('CRON');
|
|
||||||
common.statsComputed.restore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('private messages', async () => {
|
describe('private messages', async () => {
|
||||||
let lastMessageId;
|
let lastMessageId;
|
||||||
|
|
||||||
@@ -1606,7 +1543,7 @@ describe('cron', async () => {
|
|||||||
await cron({
|
await cron({
|
||||||
user, tasksByType, daysMissed, analytics,
|
user, tasksByType, daysMissed, analytics,
|
||||||
});
|
});
|
||||||
expect(user.notifications.length).to.be.greaterThan(1);
|
expect(user.notifications.length).to.eql(1);
|
||||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1820,64 +1757,258 @@ describe('cron', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('recoverCron', async () => {
|
describe('cron wrapper', () => {
|
||||||
let locals; let status; let
|
let res; let
|
||||||
execStub;
|
req;
|
||||||
|
let user;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
execStub = sandbox.stub();
|
res = generateRes();
|
||||||
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
req = generateReq();
|
||||||
|
user = await res.locals.user.save();
|
||||||
status = { times: 0 };
|
res.analytics = analytics;
|
||||||
locals = {
|
|
||||||
user: new User({
|
|
||||||
auth: {
|
|
||||||
local: {
|
|
||||||
username: 'username',
|
|
||||||
lowerCaseUsername: 'username',
|
|
||||||
email: 'email@example.com',
|
|
||||||
salt: 'salt',
|
|
||||||
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(() => {
|
||||||
sandbox.restore();
|
sandbox.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if user cannot be found', async () => {
|
it('calls next when user is not attached', async () => {
|
||||||
execStub.returns(Promise.resolve(null));
|
res.locals.user = null;
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls next when days have not been missed', async () => {
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear todos older than 30 days for free users', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||||
|
task.completed = true;
|
||||||
|
await task.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||||
|
expect(taskRes).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clear todos older than 30 days for subscribed users', async () => {
|
||||||
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
||||||
|
task.completed = true;
|
||||||
|
await Promise.all([task.save(), user.save()]);
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||||
|
expect(taskRes).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear todos older than 90 days for subscribed users', async () => {
|
||||||
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
|
||||||
|
const task = generateTodo(user);
|
||||||
|
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
||||||
|
task.completed = true;
|
||||||
|
await task.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const taskRes = await Tasks.Task.findOne({ _id: task._id });
|
||||||
|
expect(taskRes).to.not.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next if user was not modified after cron', async () => {
|
||||||
|
const hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
expect(hpBefore).to.equal(user.stats.hp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runs cron if previous cron was incomplete', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||||
|
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
||||||
|
const now = new Date();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||||
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const now = new Date();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||||
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does damage for missing dailies', async () => {
|
||||||
|
const hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const daily = generateDaily(user);
|
||||||
|
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||||
|
await daily.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const updatedUser = await User.findOne({ _id: user._id });
|
||||||
|
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates tasks', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const todo = generateTodo(user);
|
||||||
|
const todoValueBefore = todo.value;
|
||||||
|
await Promise.all([todo.save(), user.save()]);
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||||
|
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates large number of tasks', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const todo = generateTodo(user);
|
||||||
|
const todoValueBefore = todo.value;
|
||||||
|
const start = new Date();
|
||||||
|
const saves = [todo.save(), user.save()];
|
||||||
|
for (let i = 0; i < 200; i += 1) {
|
||||||
|
const newTodo = generateTodo(user);
|
||||||
|
newTodo.value = i;
|
||||||
|
saves.push(newTodo.save());
|
||||||
|
}
|
||||||
|
await Promise.all(saves);
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const duration = new Date() - start;
|
||||||
|
expect(duration).to.be.lessThan(1000);
|
||||||
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||||
|
expect(moment(start).isSame(user.lastCron, 'day'));
|
||||||
|
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||||
|
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails entire cron if one task is failing', async () => {
|
||||||
|
const lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
user.lastCron = lastCron;
|
||||||
|
const todo = generateTodo(user);
|
||||||
|
const todoValueBefore = todo.value;
|
||||||
|
const badTodo = generateTodo(user);
|
||||||
|
badTodo.text = 'bad todo';
|
||||||
|
badTodo.attribute = 'bad';
|
||||||
|
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await recoverCron(status, locals);
|
await cronWrapper(req, res);
|
||||||
throw new Error('no exception when user cannot be found');
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
expect(err).to.exist;
|
||||||
|
}
|
||||||
|
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
|
||||||
|
expect(moment(lastCron).isSame(user.lastCron, 'day'));
|
||||||
|
expect(todoFound.value).to.be.equal(todoValueBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies quest progress', async () => {
|
||||||
|
const hpBefore = user.stats.hp;
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const daily = generateDaily(user);
|
||||||
|
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
||||||
|
await daily.save();
|
||||||
|
|
||||||
|
const questKey = 'dilatory';
|
||||||
|
user.party.quest.key = questKey;
|
||||||
|
|
||||||
|
const party = new Group({
|
||||||
|
type: 'party',
|
||||||
|
name: generateUUID(),
|
||||||
|
leader: user._id,
|
||||||
|
});
|
||||||
|
party.quest.members[user._id] = true;
|
||||||
|
party.quest.key = questKey;
|
||||||
|
await party.save();
|
||||||
|
|
||||||
|
user.party._id = party._id;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
party.startQuest(user);
|
||||||
|
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
const updatedUser = await User.findOne({ _id: user._id });
|
||||||
|
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cronSignature less than 5 minutes ago should error', async () => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
const now = new Date();
|
||||||
|
await User.updateOne({
|
||||||
|
_id: user._id,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||||
|
},
|
||||||
|
}).exec();
|
||||||
|
await user.save();
|
||||||
|
try {
|
||||||
|
await cronWrapper(req, res);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.exist;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('increases status.times count and reruns up to 4 times', async () => {
|
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
|
const now = new Date();
|
||||||
|
await User.updateOne({
|
||||||
|
_id: user._id,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||||
|
},
|
||||||
|
}).exec();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
await recoverCron(status, locals);
|
await cronWrapper(req, res);
|
||||||
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||||
expect(status.times).to.eql(4);
|
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||||
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if recoverCron runs 5 times', async () => {
|
it('cron should not run more than once', async () => {
|
||||||
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
|
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||||
|
await user.save();
|
||||||
|
|
||||||
try {
|
const result = await Promise.allSettled([
|
||||||
await recoverCron(status, locals);
|
cronWrapper(req, res),
|
||||||
throw new Error('no exception when recoverCron runs 5 times');
|
cronWrapper(req, res),
|
||||||
} catch (err) {
|
new Promise((resolve, reject) => {
|
||||||
expect(status.times).to.eql(5);
|
setTimeout(async () => {
|
||||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
try {
|
||||||
}
|
const runResult = await cronWrapper(req, res);
|
||||||
|
if (runResult !== null) {
|
||||||
|
reject(new Error('cron ran more than once'));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
|
||||||
|
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -171,23 +171,23 @@ describe('emails', () => {
|
|||||||
expect(got.post).not.to.be.called;
|
expect(got.post).not.to.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when mail target is only a string', () => {
|
it('throws error when mail target is only a string', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = 'my email';
|
const mailingInfo = 'my email';
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when mail target has no _id or email', () => {
|
it('throws error when mail target has no _id or email', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when variables not an array', () => {
|
it('throws error when variables not an array', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
@@ -195,9 +195,10 @@ describe('emails', () => {
|
|||||||
};
|
};
|
||||||
const variables = {};
|
const variables = {};
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
|
||||||
});
|
});
|
||||||
it('throws error when variables array not contain name/content', () => {
|
|
||||||
|
it('throws error when variables array not contain name/content', async () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
name: 'my name',
|
name: 'my name',
|
||||||
@@ -209,8 +210,9 @@ describe('emails', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
|
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws no error when variables array contain name but no content', () => {
|
it('throws no error when variables array contain name but no content', () => {
|
||||||
const emailType = 'an email type';
|
const emailType = 'an email type';
|
||||||
const mailingInfo = {
|
const mailingInfo = {
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
|
|||||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('highlights users with case-insensitive matching', async () => {
|
||||||
|
const text = '@USER: message @User2 @USER3';
|
||||||
|
const result = await highlightMentions(text);
|
||||||
|
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
|
||||||
|
});
|
||||||
|
|
||||||
it('doesn\'t highlight nonexisting users', async () => {
|
it('doesn\'t highlight nonexisting users', async () => {
|
||||||
const text = '@nouser message';
|
const text = '@nouser message';
|
||||||
const result = await highlightMentions(text);
|
const result = await highlightMentions(text);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import nconf from 'nconf';
|
|
||||||
import requireAgain from 'require-again';
|
import requireAgain from 'require-again';
|
||||||
|
|
||||||
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
const pathToMongoLib = '../../../../website/server/libs/mongodb';
|
||||||
@@ -29,22 +28,4 @@ describe('mongodb', () => {
|
|||||||
expect(string).to.equal('mongodb://hostname:3030');
|
expect(string).to.equal('mongodb://hostname:3030');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getDefaultConnectionOptions', () => {
|
|
||||||
it('returns development config when IS_PROD is false', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
|
||||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
|
||||||
|
|
||||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
|
||||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns production config when IS_PROD is true', () => {
|
|
||||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
|
||||||
const mongoLibOverride = requireAgain(pathToMongoLib);
|
|
||||||
|
|
||||||
const options = mongoLibOverride.getDefaultConnectionOptions();
|
|
||||||
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import nconf from 'nconf';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
import {
|
import {
|
||||||
generateRes,
|
generateRes,
|
||||||
generateReq,
|
generateReq,
|
||||||
} from '../../../helpers/api-unit.helper';
|
} from '../../../helpers/api-unit.helper';
|
||||||
import { authWithHeaders as authWithHeadersFactory } from '../../../../website/server/middlewares/auth';
|
|
||||||
|
const authPath = '../../../../website/server/middlewares/auth';
|
||||||
|
|
||||||
describe('auth middleware', () => {
|
describe('auth middleware', () => {
|
||||||
let res; let req; let
|
let res; let req; let
|
||||||
@@ -16,6 +19,7 @@ describe('auth middleware', () => {
|
|||||||
|
|
||||||
describe('auth with headers', () => {
|
describe('auth with headers', () => {
|
||||||
it('allows to specify a list of user field that we do not want to load', done => {
|
it('allows to specify a list of user field that we do not want to load', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
const authWithHeaders = authWithHeadersFactory({
|
const authWithHeaders = authWithHeadersFactory({
|
||||||
userFieldsToExclude: ['items'],
|
userFieldsToExclude: ['items'],
|
||||||
});
|
});
|
||||||
@@ -35,6 +39,7 @@ describe('auth middleware', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('makes sure some fields are always included', done => {
|
it('makes sure some fields are always included', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
const authWithHeaders = authWithHeadersFactory({
|
const authWithHeaders = authWithHeadersFactory({
|
||||||
userFieldsToExclude: [
|
userFieldsToExclude: [
|
||||||
'items', 'auth.timestamps',
|
'items', 'auth.timestamps',
|
||||||
@@ -60,5 +65,57 @@ describe('auth middleware', () => {
|
|||||||
return done();
|
return done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors with InvalidCredentialsError and code when token is wrong', done => {
|
||||||
|
const authWithHeadersFactory = requireAgain(authPath).authWithHeaders;
|
||||||
|
const authWithHeaders = authWithHeadersFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = 'totally-wrong-token';
|
||||||
|
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(err.name).to.equal('InvalidCredentialsError');
|
||||||
|
expect(err.code).to.equal('invalid_credentials');
|
||||||
|
expect(err.message).to.equal(res.t('invalidCredentials'));
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when ENFORCE_CLIENT_HEADER is true', () => {
|
||||||
|
let authFactory;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox.stub(nconf, 'get').withArgs('ENFORCE_CLIENT_HEADER').returns('true');
|
||||||
|
authFactory = requireAgain(authPath).authWithHeaders;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors with missingClientHeader when x-client header is not present', done => {
|
||||||
|
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = user;
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
expect(err).to.exist;
|
||||||
|
expect(err.name).to.equal('BadRequest');
|
||||||
|
expect(err.message).to.equal(res.t('missingClientHeader'));
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows request to pass when x-client header is present', done => {
|
||||||
|
const authWithHeaders = authFactory({ userFieldsToExclude: [] });
|
||||||
|
|
||||||
|
req.headers['x-api-user'] = user._id;
|
||||||
|
req.headers['x-api-key'] = user.apiToken;
|
||||||
|
req.headers['x-client'] = 'habitica-web';
|
||||||
|
|
||||||
|
authWithHeaders(req, res, err => {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(res.locals.user).to.exist;
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,332 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import { v4 as generateUUID } from 'uuid';
|
|
||||||
import {
|
|
||||||
generateRes,
|
|
||||||
generateReq,
|
|
||||||
generateTodo,
|
|
||||||
generateDaily,
|
|
||||||
} from '../../../helpers/api-unit.helper';
|
|
||||||
import cronMiddleware from '../../../../website/server/middlewares/cron';
|
|
||||||
import { model as User } from '../../../../website/server/models/user';
|
|
||||||
import { model as Group } from '../../../../website/server/models/group';
|
|
||||||
import * as Tasks from '../../../../website/server/models/task';
|
|
||||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
|
||||||
import * as cronLib from '../../../../website/server/libs/cron';
|
|
||||||
|
|
||||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
|
||||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
|
||||||
|
|
||||||
describe('cron middleware', () => {
|
|
||||||
let res; let
|
|
||||||
req;
|
|
||||||
let user;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
res = generateRes();
|
|
||||||
req = generateReq();
|
|
||||||
user = await res.locals.user.save();
|
|
||||||
res.analytics = analyticsService;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next when user is not attached', done => {
|
|
||||||
res.locals.user = null;
|
|
||||||
cronMiddleware(req, res, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls next when days have not been missed', done => {
|
|
||||||
cronMiddleware(req, res, done);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear todos older than 30 days for free users', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.not.exist;
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not clear todos older than 30 days for subscribed users', async () => {
|
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.exist;
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear todos older than 90 days for subscribed users', async () => {
|
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
|
|
||||||
const task = generateTodo(user);
|
|
||||||
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
|
|
||||||
task.completed = true;
|
|
||||||
await task.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
Tasks.Task.findOne({ _id: task }).then(foundTask => {
|
|
||||||
expect(foundTask).to.not.exist;
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call next if user was not modified after cron', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(hpBefore).to.equal(user.stats.hp);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runs cron if previous cron was incomplete', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
|
||||||
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
|
|
||||||
const now = new Date();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.lastCron, 'day'));
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does damage for missing dailies', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const daily = generateDaily(user);
|
|
||||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await daily.save();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
|
||||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates tasks', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const todo = generateTodo(user);
|
|
||||||
const todoValueBefore = todo.value;
|
|
||||||
await Promise.all([todo.save(), user.save()]);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
|
|
||||||
expect(todoFound.value).to.be.lessThan(todoValueBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('applies quest progress', async () => {
|
|
||||||
const hpBefore = user.stats.hp;
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const daily = generateDaily(user);
|
|
||||||
daily.startDate = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await daily.save();
|
|
||||||
|
|
||||||
const questKey = 'dilatory';
|
|
||||||
user.party.quest.key = questKey;
|
|
||||||
|
|
||||||
const party = new Group({
|
|
||||||
type: 'party',
|
|
||||||
name: generateUUID(),
|
|
||||||
leader: user._id,
|
|
||||||
});
|
|
||||||
party.quest.members[user._id] = true;
|
|
||||||
party.quest.key = questKey;
|
|
||||||
await party.save();
|
|
||||||
|
|
||||||
user.party._id = party._id;
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
party.startQuest(user);
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return User.findOne({ _id: user._id }).then(updatedUser => {
|
|
||||||
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('recovers from failed cron and does not error when user is already cronning', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
const updatedUser = user.toObject();
|
|
||||||
updatedUser.matchedCount = 0;
|
|
||||||
|
|
||||||
sandbox.spy(cronLib, 'recoverCron');
|
|
||||||
|
|
||||||
sandbox.stub(User, 'updateOne')
|
|
||||||
.withArgs({
|
|
||||||
_id: user._id,
|
|
||||||
$or: [
|
|
||||||
{ _cronSignature: 'NOT_RUNNING' },
|
|
||||||
{ _cronSignature: { $lt: sinon.match.number } },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.returns({
|
|
||||||
exec () {
|
|
||||||
return Promise.resolve(updatedUser);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(cronLib.recoverCron).to.be.calledOnce;
|
|
||||||
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cronSignature less than an hour ago should error', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await User.updateOne({
|
|
||||||
_id: user._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
|
||||||
},
|
|
||||||
}).exec();
|
|
||||||
await user.save();
|
|
||||||
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (!err) return reject(new Error('Cron should have failed.'));
|
|
||||||
expect(err.message).to.be.equal(expectedErrMessage);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
const now = new Date();
|
|
||||||
await User.updateOne({
|
|
||||||
_id: user._id,
|
|
||||||
}, {
|
|
||||||
$set: {
|
|
||||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
|
||||||
},
|
|
||||||
}).exec();
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
|
||||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cron should not run more than once', async () => {
|
|
||||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
sandbox.spy(cronLib, 'cron');
|
|
||||||
|
|
||||||
await Promise.all([new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}), new Promise((resolve, reject) => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}), new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
cronMiddleware(req, res, err => {
|
|
||||||
if (err) return reject(err);
|
|
||||||
return resolve();
|
|
||||||
});
|
|
||||||
}, 400);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(cronLib.cron).to.be.calledOnce;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -238,6 +238,18 @@ describe('POST /chat', () => {
|
|||||||
expect(groupMessages[0].id).to.exist;
|
expect(groupMessages[0].id).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates a chat with case-insensitive mentions', async () => {
|
||||||
|
const originalUsername = member.auth.local.username;
|
||||||
|
const uppercaseUsername = originalUsername.toUpperCase();
|
||||||
|
const messageWithMentions = `hi @${uppercaseUsername}`;
|
||||||
|
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
|
||||||
|
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||||
|
|
||||||
|
expect(newMessage.message.id).to.exist;
|
||||||
|
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
|
||||||
|
expect(groupMessages[0].id).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
it('creates a chat with a max length of 3000 chars', async () => {
|
it('creates a chat with a max length of 3000 chars', async () => {
|
||||||
const veryLongMessage = `
|
const veryLongMessage = `
|
||||||
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
|
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
requester,
|
requester,
|
||||||
translate as t,
|
translate as t,
|
||||||
|
generateUser,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import i18n from '../../../../../website/common/script/i18n';
|
import i18n from '../../../../../website/common/script/i18n';
|
||||||
|
|
||||||
@@ -56,4 +57,28 @@ describe('GET /content', () => {
|
|||||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||||
expect(res).to.not.have.property('backgroundsFlat');
|
expect(res).to.not.have.property('backgroundsFlat');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('authenticated user', () => {
|
||||||
|
let user;
|
||||||
|
it('returns content in user\'s preferred language when no language parameter is provided', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'de' });
|
||||||
|
const res = await user.get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects language parameter over user\'s preferred language', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'de' });
|
||||||
|
const res = await user.get('/content?language=fr');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to English if user\'s preferred language is invalid', async () => {
|
||||||
|
user = await generateUser({ 'preferences.language': 'invalid_lang' });
|
||||||
|
const res = await user.get('/content');
|
||||||
|
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||||
|
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ describe('GET /heroes/:heroId', () => {
|
|||||||
const heroFields = [
|
const heroFields = [
|
||||||
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
|
||||||
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
|
||||||
|
'stats',
|
||||||
];
|
];
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
const heroFields = [
|
const heroFields = [
|
||||||
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
|
||||||
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
|
||||||
|
'stats',
|
||||||
];
|
];
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ describe('GET /members/username/:username', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a member public data only', async () => {
|
it('returns a member\'s public data only', async () => {
|
||||||
// make sure user has all the fields that can be returned by the getMember call
|
// make sure user has all the fields that can be returned by the getMember call
|
||||||
const member = await generateUser({
|
const member = await generateUser({
|
||||||
contributor: { level: 1 },
|
contributor: { level: 1 },
|
||||||
|
|||||||
@@ -101,34 +101,6 @@ describe('GET /tasks/user', () => {
|
|||||||
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
|
|
||||||
const LIMIT = 30;
|
|
||||||
const numberOfTodos = LIMIT + 1;
|
|
||||||
const todosInput = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
|
||||||
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
|
|
||||||
}
|
|
||||||
const todos = await user.post('/tasks/user', todosInput);
|
|
||||||
await user.sync();
|
|
||||||
const initialTodoCount = user.tasksOrder.todos.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < numberOfTodos; i += 1) {
|
|
||||||
const id = todos[i]._id;
|
|
||||||
|
|
||||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
|
||||||
}
|
|
||||||
await user.sync();
|
|
||||||
|
|
||||||
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
|
|
||||||
|
|
||||||
const completedTodos = await user.get('/tasks/user?type=completedTodos');
|
|
||||||
expect(completedTodos.length).to.equal(LIMIT);
|
|
||||||
|
|
||||||
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
|
|
||||||
expect(allCompletedTodos.length).to.equal(numberOfTodos);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns dailies with isDue for the date specified', async () => {
|
it('returns dailies with isDue for the date specified', async () => {
|
||||||
// @TODO Add required format
|
// @TODO Add required format
|
||||||
const startDate = moment().subtract('1', 'days').toISOString();
|
const startDate = moment().subtract('1', 'days').toISOString();
|
||||||
|
|||||||
@@ -238,6 +238,28 @@ describe('POST /user/auth/reset-password-set-new-one', () => {
|
|||||||
expect(isPassValid).to.equal(true);
|
expect(isPassValid).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('changes the apiToken on password reset', async () => {
|
||||||
|
const user = await generateUser();
|
||||||
|
const previousToken = user.apiToken;
|
||||||
|
|
||||||
|
const code = encrypt(JSON.stringify({
|
||||||
|
userId: user._id,
|
||||||
|
expiresAt: moment().add({ days: 1 }),
|
||||||
|
}));
|
||||||
|
await user.updateOne({
|
||||||
|
'auth.local.passwordResetCode': code,
|
||||||
|
});
|
||||||
|
|
||||||
|
await api.post(`${endpoint}`, {
|
||||||
|
newPassword: 'my new password',
|
||||||
|
confirmPassword: 'my new password',
|
||||||
|
code,
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
expect(user.apiToken).to.not.eql(previousToken);
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
it('renders the success page and convert the password from sha1 to bcrypt', async () => {
|
||||||
const user = await generateUser();
|
const user = await generateUser();
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,30 @@ describe('PUT /user/auth/update-password', async () => {
|
|||||||
newPassword,
|
newPassword,
|
||||||
confirmPassword: newPassword,
|
confirmPassword: newPassword,
|
||||||
});
|
});
|
||||||
expect(response).to.eql({});
|
|
||||||
|
expect(response).to.exist;
|
||||||
|
expect(response.apiToken).to.exist;
|
||||||
|
|
||||||
await user.sync();
|
await user.sync();
|
||||||
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
expect(user.auth.local.hashed_password).to.not.eql(previousHashedPassword);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should change the apiToken on password change', async () => {
|
||||||
|
const previousToken = user.apiToken;
|
||||||
|
const response = await user.put(ENDPOINT, {
|
||||||
|
password,
|
||||||
|
newPassword,
|
||||||
|
confirmPassword: newPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newToken = response.apiToken;
|
||||||
|
expect(newToken).to.exist;
|
||||||
|
|
||||||
|
await user.sync();
|
||||||
|
expect(user.apiToken).to.eql(newToken);
|
||||||
|
expect(user.apiToken).to.not.eql(previousToken);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an error when confirmPassword does not match newPassword', async () => {
|
it('returns an error when confirmPassword does not match newPassword', async () => {
|
||||||
await expect(user.put(ENDPOINT, {
|
await expect(user.put(ENDPOINT, {
|
||||||
password,
|
password,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
|||||||
userToSendMessage = await generateUser();
|
userToSendMessage = await generateUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns an error when private message is not found', async () => {
|
it('returns an error when private message is not found', async () => {
|
||||||
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||||
.to.eventually.be.rejected.and.eql({
|
.to.eventually.be.rejected.and.eql({
|
||||||
code: 404,
|
code: 404,
|
||||||
@@ -35,7 +35,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Likes a message', async () => {
|
it('likes a message', async () => {
|
||||||
const receiver = await generateUser();
|
const receiver = await generateUser();
|
||||||
|
|
||||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
@@ -57,7 +57,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
|||||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Allows to likes their own private message', async () => {
|
it('allows a user to like their own private message', async () => {
|
||||||
const receiver = await generateUser();
|
const receiver = await generateUser();
|
||||||
|
|
||||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
@@ -78,7 +78,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
|
|||||||
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Unlikes a message', async () => {
|
it('unlikes a message', async () => {
|
||||||
const receiver = await generateUser();
|
const receiver = await generateUser();
|
||||||
|
|
||||||
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty array when no events are active', () => {
|
it('returns empty array when no events are active', () => {
|
||||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
clock = sinon.useFakeTimers(new Date('2024-01-11'));
|
||||||
const events = getRepeatingEvents();
|
const events = getRepeatingEvents();
|
||||||
expect(events).to.be.empty;
|
expect(events).to.be.empty;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,21 +133,21 @@ describe('Content Schedule', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sets the end date for a gala', () => {
|
it('sets the end date for a gala', () => {
|
||||||
const date = new Date('2024-05-20');
|
const date = new Date('2024-05-31');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the end date for a winter gala', () => {
|
it('sets the end date for a winter gala', () => {
|
||||||
const date = new Date('2024-12-22');
|
const date = new Date('2025-02-28');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the end date in new year for a winter gala', () => {
|
it('sets the end date in new year for a winter gala', () => {
|
||||||
const date = new Date('2025-01-04');
|
const date = new Date('2025-02-28');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-01T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses correct date for first hours of the month', () => {
|
it('uses correct date for first hours of the month', () => {
|
||||||
@@ -190,7 +190,7 @@ describe('Content Schedule', () => {
|
|||||||
const date = new Date('2024-04-15');
|
const date = new Date('2024-04-15');
|
||||||
const matchers = getAllScheduleMatchingGroups(date);
|
const matchers = getAllScheduleMatchingGroups(date);
|
||||||
expect(matchers.premiumHatchingPotions).to.exist;
|
expect(matchers.premiumHatchingPotions).to.exist;
|
||||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
|
||||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('Shop Featured Items', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('contains the current premium hatching potions', () => {
|
it('contains the current premium hatching potions', () => {
|
||||||
clock = Sinon.useFakeTimers(new Date('2024-04-08'));
|
clock = Sinon.useFakeTimers(new Date('2024-04-09'));
|
||||||
const items = featuredItems.market();
|
const items = featuredItems.market();
|
||||||
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
expect(_.find(items, item => item.path === 'premiumHatchingPotions.Porcelain')).to.exist;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
|
|||||||
sinonStubPromise(global.sinon);
|
sinonStubPromise(global.sinon);
|
||||||
global.sandbox = sinon.createSandbox();
|
global.sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||||
|
|
||||||
setupNconf('./config.json.example');
|
setupNconf('./config.json.example');
|
||||||
|
|||||||
@@ -74,15 +74,10 @@ export async function getDocument (collectionName, doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
before(done => {
|
before(done => {
|
||||||
mongoose.connection.on('open', err => {
|
mongoose.connection.once('open', async err => {
|
||||||
if (err) return done(err);
|
if (err) throw err;
|
||||||
return resetHabiticaDB()
|
await resetHabiticaDB();
|
||||||
.then(() => {
|
done();
|
||||||
done();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
const nconf = require('nconf');
|
const nconf = require('nconf');
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const setupNconf = require('../../website/server/libs/setupNconf');
|
const setupNconf = require('../../website/server/libs/setupNconf').default;
|
||||||
|
|
||||||
// fix further imports of require/import syntaxes
|
// fix further imports of require/import syntaxes
|
||||||
require('@babel/register');
|
require('@babel/register');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
es2021: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'habitrpg/lib/vue',
|
'habitrpg/lib/vue',
|
||||||
@@ -39,7 +40,4 @@ module.exports = {
|
|||||||
order: ['template', 'style', 'script'],
|
order: ['template', 'style', 'script'],
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
parserOptions: {
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/* eslint-disable import/no-commonjs */
|
|
||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
'@vue/cli-plugin-babel/preset',
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-proposal-optional-chaining',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||||
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
||||||
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="loading-screen">
|
<div id="loading-screen">
|
||||||
@@ -28,10 +29,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
|
||||||
|
|
||||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||||
<!-- Translations -->
|
<!-- Translations -->
|
||||||
<script type='text/javascript' src='/api/v4/i18n/browser-script'></script>
|
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
11833
website/client/package-lock.json
generated
11833
website/client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,28 +3,26 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vite",
|
||||||
"build": "vue-cli-service build",
|
"build": "vite build",
|
||||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
"preview": "vite preview",
|
||||||
"lint": "vue-cli-service lint .",
|
"test:unit": "vitest run",
|
||||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
"test:unit:watch": "vitest watch",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix .",
|
||||||
|
"lint-no-fix": "eslint --ext .js,.vue --no-fix src",
|
||||||
"postinstall": "node ./scripts/npm-postinstall.js"
|
"postinstall": "node ./scripts/npm-postinstall.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.8",
|
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||||
"@vue/cli-plugin-eslint": "^5.0.8",
|
"@vitejs/plugin-vue2": "^2.3.3",
|
||||||
"@vue/cli-plugin-router": "^5.0.8",
|
|
||||||
"@vue/cli-plugin-unit-mocha": "^5.0.8",
|
|
||||||
"@vue/cli-service": "^5.0.8",
|
|
||||||
"@vue/test-utils": "1.0.0-beta.29",
|
"@vue/test-utils": "1.0.0-beta.29",
|
||||||
"amplitude-js": "^8.21.3",
|
"amplitude-js": "^8.21.3",
|
||||||
"assert": "^2.1.0",
|
"assert": "^2.1.0",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"axios": "^0.28.0",
|
"axios": "^0.28.0",
|
||||||
"axios-progress-bar": "^1.2.0",
|
"axios-progress-bar": "^1.2.0",
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"bootstrap": "^4.6.0",
|
"bootstrap": "^4.6.0",
|
||||||
"bootstrap-vue": "^2.23.1",
|
"bootstrap-vue": "^2.23.1",
|
||||||
"core-js": "^3.33.1",
|
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-habitrpg": "6.2.0",
|
"eslint-config-habitrpg": "6.2.0",
|
||||||
"eslint-plugin-mocha": "5.3.0",
|
"eslint-plugin-mocha": "5.3.0",
|
||||||
@@ -34,31 +32,34 @@
|
|||||||
"intro.js": "^7.2.0",
|
"intro.js": "^7.2.0",
|
||||||
"jquery": "^3.7.1",
|
"jquery": "^3.7.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"moment-locales-webpack-plugin": "^1.2.0",
|
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"sass": "^1.63.4",
|
"sass": "^1.63.4",
|
||||||
"sass-loader": "^14.1.1",
|
|
||||||
"sinon": "^17.0.1",
|
"sinon": "^17.0.1",
|
||||||
"stopword": "^2.0.8",
|
"stopword": "^2.0.8",
|
||||||
"timers-browserify": "^2.0.12",
|
"timers-browserify": "^2.0.12",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"validator": "^13.9.0",
|
"validator": "^13.9.0",
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"vite-plugin-compression2": "^1.3.3",
|
||||||
"vue": "^2.7.10",
|
"vue": "^2.7.10",
|
||||||
"vue-fragment": "^1.6.0",
|
"vue-fragment": "^1.6.0",
|
||||||
"vue-mugen-scroll": "^0.2.6",
|
"vue-mugen-scroll": "^0.2.6",
|
||||||
"vue-router": "^3.6.5",
|
"vue-router": "^3.6.5",
|
||||||
"vue-template-babel-compiler": "^2.0.0",
|
|
||||||
"vue-template-compiler": "^2.7.10",
|
|
||||||
"vuedraggable": "^2.24.3",
|
"vuedraggable": "^2.24.3",
|
||||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
|
"@vitest/browser": "^3.0.5",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "^3.3.4",
|
||||||
"chai": "^5.1.0",
|
|
||||||
"inspectpack": "^4.7.1",
|
"inspectpack": "^4.7.1",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"mocha": "^11.1.0",
|
||||||
|
"playwright": "^1.50.1",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
|
"vitest": "^3.0.5",
|
||||||
"webpack": "^5.94.0"
|
"webpack": "^5.94.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<snackbars />
|
<snackbars />
|
||||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||||
<user-main v-else />
|
<div v-else>
|
||||||
|
<user-main />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#loading-screen-inapp {
|
#loading-screen-inapp {
|
||||||
#melior {
|
#melior {
|
||||||
@@ -90,7 +92,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang='scss'>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
opacity: .9 !important;
|
opacity: .9 !important;
|
||||||
@@ -108,16 +110,16 @@ import axios from 'axios';
|
|||||||
|
|
||||||
import * as Analytics from '@/libs/analytics';
|
import * as Analytics from '@/libs/analytics';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import userMain from '@/pages/user-main';
|
|
||||||
import snackbars from '@/components/snackbars/notifications';
|
import snackbars from '@/components/snackbars/notifications';
|
||||||
|
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
snackbars,
|
snackbars,
|
||||||
userMain,
|
userMain: () => import('@/pages/user-main'),
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -221,11 +223,10 @@ export default {
|
|||||||
|
|
||||||
const errorData = error.response.data;
|
const errorData = error.response.data;
|
||||||
const errorMessage = errorData.message || errorData;
|
const errorMessage = errorData.message || errorData;
|
||||||
|
const errorCode = errorData.error;
|
||||||
|
|
||||||
// Check for conditions to reset the user auth
|
// If 'invalid_credentials' signaled, force logout
|
||||||
// TODO use a specific error like NotificationNotFound instead of checking for the string
|
if (error.response.status === 401 && errorCode === 'invalid_credentials') {
|
||||||
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
|
|
||||||
if (invalidUserMessage.indexOf(errorMessage) !== -1) {
|
|
||||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -268,16 +269,29 @@ export default {
|
|||||||
const loadingScreen = document.getElementById('loading-screen');
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||||
|
|
||||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
// Check if we need to show password change success message
|
||||||
this.hideLoadingScreen();
|
if (sessionStorage.getItem('passwordChangeSuccess') === 'true') {
|
||||||
|
sessionStorage.removeItem('passwordChangeSuccess');
|
||||||
|
this.$store.dispatch('snackbars:add', {
|
||||||
|
title: 'Habitica',
|
||||||
|
text: this.$t('passwordSuccess'),
|
||||||
|
type: 'success',
|
||||||
|
timeout: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$router.onReady(() => {
|
||||||
|
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||||
|
this.hideLoadingScreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hideLoadingScreen () {
|
hideLoadingScreen () {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
},
|
},
|
||||||
checkForBannedUser (error) {
|
checkForBannedUser (error) {
|
||||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||||
const errorMessage = error.response.data.message;
|
const errorMessage = error.response.data.message;
|
||||||
|
|
||||||
@@ -301,4 +315,3 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@
|
|||||||
height: 219px;
|
height: 219px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
|
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||||
|
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||||
width: 68px;
|
width: 68px;
|
||||||
height: 68px;
|
height: 68px;
|
||||||
}
|
}
|
||||||
@@ -47,6 +48,10 @@
|
|||||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Pet_HatchingPotion_Cryptid {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.Gems {
|
.Gems {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
margin-right:5px;
|
margin-right:5px;
|
||||||
@@ -172,7 +177,7 @@
|
|||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
|
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice, .Mount_Head_Dragon-Hydra, .Mount_Body_Dragon-Hydra {
|
||||||
width: 135px;
|
width: 135px;
|
||||||
height: 135px;
|
height: 135px;
|
||||||
}
|
}
|
||||||
@@ -185,6 +190,14 @@
|
|||||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Mount_Head_Dragon-Hydra {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Mount_Body_Dragon-Hydra {
|
||||||
|
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.background_airship, .background_clocktower, .background_steamworks {
|
.background_airship, .background_clocktower, .background_steamworks {
|
||||||
width: 141px;
|
width: 141px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
|||||||
top: -16px !important;
|
top: -16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
|
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
|
||||||
|
|
||||||
@each $foolPet in $foolPets {
|
@each $foolPet in $foolPets {
|
||||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.featured-label {
|
.featured-label {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
$grid-gutter-width: 24px;
|
$grid-gutter-width: 24px;
|
||||||
|
|
||||||
// Bootstrap and its default variables
|
// Bootstrap and its default variables
|
||||||
@import 'node_modules/bootstrap/scss/bootstrap';
|
@import '~/bootstrap/scss/bootstrap';
|
||||||
|
|
||||||
// Bootstrap Vue styles
|
// Bootstrap Vue styles
|
||||||
@import 'node_modules/bootstrap-vue/dist/bootstrap-vue';
|
@import '~/bootstrap-vue/dist/bootstrap-vue';
|
||||||
@@ -316,3 +316,9 @@
|
|||||||
line-height: 2;
|
line-height: 2;
|
||||||
padding: 2px 2px;
|
padding: 2px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-lg {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
|
|||||||
@@ -61,13 +61,13 @@ input, textarea, input.form-control, textarea.form-control {
|
|||||||
|
|
||||||
&.input-valid {
|
&.input-valid {
|
||||||
padding-right: 27px;
|
padding-right: 27px;
|
||||||
background-image: url(~@/assets/svg/for-css/check.svg);
|
background-image: url(@/assets/svg/for-css/check.svg);
|
||||||
background-size: 1rem;
|
background-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.input-invalid {
|
&.input-invalid {
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
background-image: url(~@/assets/svg/for-css/alert.svg);
|
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
border-color: $red-100 !important;
|
border-color: $red-100 !important;
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ $bg-disabled-control: $gray-10;
|
|||||||
&:checked~.custom-control-label::after {
|
&:checked~.custom-control-label::after {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||||
background-size: 13px 10px;
|
background-size: 13px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,13 +29,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.iconalert-success::before {
|
.iconalert-success::before {
|
||||||
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
|
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||||
background-size: 13px 10px;
|
background-size: 13px 10px;
|
||||||
background-color: #1ca372;
|
background-color: #1ca372;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconalert-warning::before, .iconalert-error::before {
|
.iconalert-warning::before, .iconalert-error::before {
|
||||||
background-image: url(~@/assets/svg/for-css/alert-white.svg);
|
background-image: url(@/assets/svg/for-css/alert-white.svg);
|
||||||
background-size: 16px 16px;
|
background-size: 16px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
z-index: 1350;
|
z-index: 1350;
|
||||||
|
|||||||
@@ -46,13 +46,11 @@
|
|||||||
|
|
||||||
.background {
|
.background {
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
|
height:216px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -67,6 +65,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shop-message {
|
||||||
|
position: relative;
|
||||||
|
height: 76px;
|
||||||
|
margin: 71px auto;
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
.npc {
|
.npc {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.container-fluid.static-view {
|
.container-fluid.static-view {
|
||||||
margin: 5em 2em 0 2em;
|
margin: 5em 2em 0 2em;
|
||||||
|
|||||||
3
website/client/src/assets/svg/bluesky.svg
Normal file
3
website/client/src/assets/svg/bluesky.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4,0C1.79,0,0,1.79,0,4v16c0,2.21,1.79,4,4,4h16c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4H4ZM12,11.57c-.72-1.49-2.7-4.26-4.53-5.63-1.32-.99-3.47-1.75-3.47.68,0,.49.28,4.08.44,4.66.57,2.03,2.65,2.55,4.5,2.23-3.24.55-4.06,2.36-2.28,4.17,3.38,3.44,4.85-.86,5.23-1.97h0s0,0,0,0c.07-.2.1-.29.1-.21,0-.08.03.01.1.22h0c.38,1.1,1.85,5.41,5.23,1.97,1.78-1.81.95-3.63-2.28-4.17,1.85.31,3.93-.2,4.5-2.23.16-.58.44-4.18.44-4.66,0-2.43-2.14-1.67-3.47-.68-1.83,1.37-3.81,4.14-4.53,5.63Z" fill-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.1792 31.6843L46.8536 22.3769L23.918 28.6988L18.861 42.5218L44.341 58.5813L58.1792 31.6843Z" fill="#FF944C"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L46.1108 26.1328L36.2812 28.8422L46.6218 34.5148Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M30.2393 39.0304L26.4518 31.5515L36.2813 28.8422L30.2393 39.0304Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L36.2813 28.8422L30.2393 39.0304L46.6218 34.5148Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.1108 26.1328L46.6218 34.5148L53.8301 32.5279Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L26.4518 31.5516L30.2393 39.0304L23.0309 41.0173Z" fill="#FA8537"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.6218 34.5148L43.0424 53.79L53.8301 32.5279Z" fill="#FA8537"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L30.2393 39.0304L43.0425 53.79L23.0309 41.0173Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L30.2393 39.0304L43.0425 53.79L46.6218 34.5148Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.555 4.15937L47.026 0.420004L38.7773 1.59601L36.4144 6.17539L44.5675 12.8919L50.555 4.15937Z" fill="#FFBE5D"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L46.6034 1.6924L43.0682 2.1964L46.414 4.62854Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M40.5221 5.46854L39.5331 2.7004L43.0682 2.1964L40.5221 5.46854Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L43.0683 2.1964L40.5221 5.46855L46.414 4.62854Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25894L46.6034 1.6924L46.414 4.62854L49.0064 4.25894Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M37.9296 5.83815L39.5331 2.70041L40.5221 5.46855L37.9296 5.83815Z" fill="#FFA624"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25893L46.414 4.62853L44.3259 11.1688L49.0064 4.25893Z" fill="#FFA624"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M37.9297 5.83815L40.5221 5.46855L44.326 11.1688L37.9297 5.83815Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L40.5221 5.46855L44.326 11.1688L46.414 4.62854Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2986 16.7775L24.6513 8.36623L11.1016 3.94533L4.07056 9.19883L11.614 25.6769L27.2986 16.7775Z" fill="#FF6165"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L23.0573 10.0026L17.2502 8.10789L20.5864 14.3719Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M10.908 11.2141L11.4432 6.21322L17.2502 8.10789L10.908 11.2141Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L17.2502 8.10789L10.9081 11.2141L20.5864 14.3719Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L23.0573 10.0026L20.5864 14.3719L24.8449 15.7613Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M6.64955 9.82464L11.4432 6.21321L10.908 11.2141L6.64955 9.82464Z" fill="#F23035"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L20.5864 14.3719L12.5221 22.8464L24.8449 15.7613Z" fill="#F23035"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.64959 9.82464L10.9081 11.2141L12.5221 22.8463L6.64959 9.82464Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L10.9081 11.2141L12.5221 22.8463L20.5864 14.3719Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.82083 31.6843L17.1464 22.3769L40.082 28.6988L45.139 42.5218L19.659 58.5813L5.82083 31.6843Z" fill="#24CC8F"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L17.8892 26.1328L27.7188 28.8422L17.3782 34.5148Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.7607 39.0304L37.5482 31.5515L27.7187 28.8422L33.7607 39.0304Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L27.7187 28.8422L33.7607 39.0304L17.3782 34.5148Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.8892 26.1328L17.3782 34.5148L10.1699 32.5279Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L37.5482 31.5516L33.7607 39.0304L40.9691 41.0173Z" fill="#1CA372"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.3782 34.5148L20.9576 53.79L10.1699 32.5279Z" fill="#1CA372"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L33.7607 39.0304L20.9575 53.79L40.9691 41.0173Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L33.7607 39.0304L20.9575 53.79L17.3782 34.5148Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.445 4.15937L16.974 0.420004L25.2227 1.59601L27.5856 6.17539L19.4325 12.8919L13.445 4.15937Z" fill="#925CF3"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L17.3966 1.6924L20.9318 2.1964L17.586 4.62854Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M23.4779 5.46854L24.4669 2.7004L20.9318 2.1964L23.4779 5.46854Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L20.9317 2.1964L23.4779 5.46855L17.586 4.62854Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25894L17.3966 1.6924L17.586 4.62854L14.9936 4.25894Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M26.0704 5.83815L24.4669 2.70041L23.4779 5.46855L26.0704 5.83815Z" fill="#4F2A93"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25893L17.586 4.62853L19.6741 11.1688L14.9936 4.25893Z" fill="#4F2A93"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M26.0703 5.83815L23.4779 5.46855L19.674 11.1688L26.0703 5.83815Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L23.4779 5.46855L19.674 11.1688L17.586 4.62854Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7014 16.7775L39.3487 8.36623L52.8984 3.94533L59.9294 9.19883L52.386 25.6769L36.7014 16.7775Z" fill="#50B5E9"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L40.9427 10.0026L46.7498 8.10789L43.4136 14.3719Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M53.092 11.2141L52.5568 6.21322L46.7498 8.10789L53.092 11.2141Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L46.7498 8.10789L53.0919 11.2141L43.4136 14.3719Z" fill="white"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L40.9427 10.0026L43.4136 14.3719L39.1551 15.7613Z" fill="white"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L52.5568 6.21321L53.092 11.2141L57.3504 9.82464Z" fill="#46A7D9"/>
|
||||||
|
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L43.4136 14.3719L51.4779 22.8464L39.1551 15.7613Z" fill="#46A7D9"/>
|
||||||
|
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L53.0919 11.2141L51.4779 22.8463L57.3504 9.82464Z" fill="white"/>
|
||||||
|
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L53.0919 11.2141L51.4779 22.8463L43.4136 14.3719Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 622 B |
@@ -4,7 +4,7 @@
|
|||||||
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
|
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
|
||||||
<img
|
<img
|
||||||
:class="retiredChatPage ? 'mt-5' : 'image-404'"
|
:class="retiredChatPage ? 'mt-5' : 'image-404'"
|
||||||
src="~@/assets/images/404.png"
|
src="@/assets/images/404.png"
|
||||||
>
|
>
|
||||||
<div v-if="retiredChatPage">
|
<div v-if="retiredChatPage">
|
||||||
<h1>
|
<h1>
|
||||||
@@ -25,9 +25,9 @@
|
|||||||
<router-link to="/">
|
<router-link to="/">
|
||||||
Homepage
|
Homepage
|
||||||
</router-link>or
|
</router-link>or
|
||||||
<router-link :to="contactUsLink">
|
<a href="mailto:admin@habitica.com">
|
||||||
Contact Us
|
Contact Us
|
||||||
</router-link>about the issue.
|
</a>about the issue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,12 +40,6 @@ import { mapState } from '@/libs/store';
|
|||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['isUserLoggedIn']),
|
...mapState(['isUserLoggedIn']),
|
||||||
contactUsLink () {
|
|
||||||
if (this.isUserLoggedIn) {
|
|
||||||
return { name: 'guild', params: { groupId: 'a29da26b-37de-4a71-b0c6-48e72a900dac' } };
|
|
||||||
}
|
|
||||||
return { name: 'contact' };
|
|
||||||
},
|
|
||||||
retiredChatPage () {
|
retiredChatPage () {
|
||||||
return this.$route.fullPath.indexOf('/groups') !== -1;
|
return this.$route.fullPath.indexOf('/groups') !== -1;
|
||||||
},
|
},
|
||||||
@@ -54,7 +48,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h1, .static-wrapper h1 {
|
h1, .static-wrapper h1 {
|
||||||
color: $purple-200;
|
color: $purple-200;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6 offset-3">
|
<div class="col-6 offset-3">
|
||||||
<div class="shop_armoire"></div>
|
<Sprite image-name="shop_armoire" />
|
||||||
<p>{{ $t('armoireLastItem') }}</p>
|
<p>{{ $t('armoireLastItem') }}</p>
|
||||||
<p>{{ $t('armoireNotesEmpty') }}</p>
|
<p>{{ $t('armoireNotesEmpty') }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,12 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Sprite from '@/components/ui/sprite';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
Sprite,
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
close () {
|
close () {
|
||||||
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
this.$root.$emit('bv::hide::modal', 'armoire-empty');
|
||||||
|
|||||||
@@ -95,7 +95,11 @@
|
|||||||
@click="clickDisableClasses(); close();"
|
@click="clickDisableClasses(); close();"
|
||||||
>{{ $t('optOutOfClasses') }}</span>
|
>{{ $t('optOutOfClasses') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="opt-out-description">{{ $t('optOutOfClassesText') }}</span>
|
<div
|
||||||
|
v-once
|
||||||
|
class="opt-out-description"
|
||||||
|
v-html="$t('optOutOfClassesText')"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +107,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.btn-primary:active {
|
.btn-primary:active {
|
||||||
border: 2px solid $purple-400 !important;
|
border: 2px solid $purple-400 !important;
|
||||||
@@ -189,10 +193,10 @@
|
|||||||
import Avatar from '../avatar';
|
import Avatar from '../avatar';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import markdownDirective from '@/directives/markdown';
|
import markdownDirective from '@/directives/markdown';
|
||||||
import warriorIcon from '@/assets/svg/warrior.svg';
|
import warriorIcon from '@/assets/svg/warrior.svg?raw';
|
||||||
import rogueIcon from '@/assets/svg/rogue.svg';
|
import rogueIcon from '@/assets/svg/rogue.svg?raw';
|
||||||
import healerIcon from '@/assets/svg/healer.svg';
|
import healerIcon from '@/assets/svg/healer.svg?raw';
|
||||||
import wizardIcon from '@/assets/svg/wizard.svg';
|
import wizardIcon from '@/assets/svg/wizard.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $purple-200;
|
color: $purple-200;
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import closeIcon from '@/assets/svg/close.svg';
|
import closeIcon from '@/assets/svg/close.svg?raw';
|
||||||
import Sprite from '@/components/ui/sprite.vue';
|
import Sprite from '@/components/ui/sprite.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~@/assets/scss/mixins.scss';
|
@import '@/assets/scss/mixins.scss';
|
||||||
|
|
||||||
#generic-achievement {
|
#generic-achievement {
|
||||||
@include centeredModal();
|
@include centeredModal();
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import achievements from '@/../../common/script/content/achievements';
|
import achievements from '@/../../common/script/content/achievements';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import svgClose from '@/assets/svg/close.svg';
|
import svgClose from '@/assets/svg/close.svg?raw';
|
||||||
import Sprite from '@/components/ui/sprite.vue';
|
import Sprite from '@/components/ui/sprite.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="questClass"></div>
|
<Sprite :image-name="questClass" />
|
||||||
</section>
|
</section>
|
||||||
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
|
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
|
||||||
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
|
||||||
@@ -58,7 +58,7 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#level-up {
|
#level-up {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -150,18 +150,15 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
|
|||||||
section.greyed {
|
section.greyed {
|
||||||
padding-bottom: 17px
|
padding-bottom: 17px
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll {
|
|
||||||
margin: -11px auto 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Avatar from '../avatar';
|
import Avatar from '../avatar';
|
||||||
|
import Sprite from '@/components/ui/sprite';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import starGroup from '@/assets/svg/star-group.svg';
|
import starGroup from '@/assets/svg/star-group.svg?raw';
|
||||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
|
||||||
|
|
||||||
const levelQuests = {
|
const levelQuests = {
|
||||||
15: 'atom1',
|
15: 'atom1',
|
||||||
@@ -173,6 +170,7 @@ const levelQuests = {
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Sprite,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -191,7 +189,9 @@ export default {
|
|||||||
return this.user.stats.lvl in levelQuests;
|
return this.user.stats.lvl in levelQuests;
|
||||||
},
|
},
|
||||||
questClass () {
|
questClass () {
|
||||||
return `scroll inventory_quest_scroll_${levelQuests[this.user.stats.lvl]}`;
|
const questKey = levelQuests[this.user.stats.lvl];
|
||||||
|
if (questKey) return `inventory_quest_scroll_${questKey}`;
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<img
|
<img
|
||||||
class="onboarding-complete-banner d-block"
|
class="onboarding-complete-banner d-block"
|
||||||
src="~@/assets/images/onboarding-complete-banner@2x.png"
|
src="@/assets/images/onboarding-complete-banner@2x.png"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
v-once
|
v-once
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $purple-200;
|
color: $purple-200;
|
||||||
@@ -100,7 +100,7 @@ button {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import svgClose from '@/assets/svg/close.svg';
|
import svgClose from '@/assets/svg/close.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
@@ -117,7 +117,7 @@ export default {
|
|||||||
closeWithAction () {
|
closeWithAction () {
|
||||||
this.close();
|
this.close();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$router.push({ name: 'achievements' });
|
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||||
}, 200);
|
}, 200);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -97,9 +97,9 @@ import { mapState } from '@/libs/store';
|
|||||||
import Sprite from '@/components/ui/sprite';
|
import Sprite from '@/components/ui/sprite';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: [
|
components: {
|
||||||
Sprite,
|
Sprite,
|
||||||
],
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
maxHealth,
|
maxHealth,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<p v-html="$t('moreGearAchievements')"></p>
|
<p v-html="$t('moreGearAchievements')"></p>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
<div class="shop_armoire"></div>
|
<Sprite image-name="shop_armoire" />
|
||||||
<p v-html="$t('armoireUnlocked')"></p>
|
<p v-html="$t('armoireUnlocked')"></p>
|
||||||
<br>
|
<br>
|
||||||
<button
|
<button
|
||||||
@@ -87,11 +87,13 @@
|
|||||||
import achievementFooter from './achievementFooter';
|
import achievementFooter from './achievementFooter';
|
||||||
import achievementAvatar from './achievementAvatar';
|
import achievementAvatar from './achievementAvatar';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
|
import Sprite from '@/components/ui/sprite.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
achievementFooter,
|
achievementFooter,
|
||||||
achievementAvatar,
|
achievementAvatar,
|
||||||
|
Sprite,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({ user: 'user.data' }),
|
...mapState({ user: 'user.data' }),
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#won-challenge {
|
#won-challenge {
|
||||||
.modal-body {
|
.modal-body {
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
color: $purple-300;
|
color: $purple-300;
|
||||||
@@ -146,9 +146,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import habiticaMarkdown from 'habitica-markdown';
|
import habiticaMarkdown from 'habitica-markdown';
|
||||||
import closeIcon from '@/components/shared/closeIcon';
|
import closeIcon from '@/components/shared/closeIcon';
|
||||||
import sparkles from '@/assets/svg/star-group.svg';
|
import sparkles from '@/assets/svg/star-group.svg?raw';
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import gem from '@/assets/svg/gem.svg?raw';
|
||||||
import stars from '@/assets/svg/sparkles-left.svg';
|
import stars from '@/assets/svg/sparkles-left.svg?raw';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -92,8 +92,6 @@ export default {
|
|||||||
params: { userIdentifier },
|
params: { userIdentifier },
|
||||||
}).catch(failure => {
|
}).catch(failure => {
|
||||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
// the admin has requested that the same user be displayed again so reload the page
|
|
||||||
// (e.g., if they changed their mind about changes they were making)
|
|
||||||
this.$router.go();
|
this.$router.go();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -101,14 +99,16 @@ export default {
|
|||||||
|
|
||||||
async loadUser (userIdentifier) {
|
async loadUser (userIdentifier) {
|
||||||
const id = userIdentifier || this.user._id;
|
const id = userIdentifier || this.user._id;
|
||||||
|
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||||
this.$router.push({
|
await this.$router.push({
|
||||||
|
name: 'adminPanel',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.$router.push({
|
||||||
name: 'adminPanelUser',
|
name: 'adminPanelUser',
|
||||||
params: { userIdentifier: id },
|
params: { userIdentifier: id },
|
||||||
}).catch(failure => {
|
}).catch(failure => {
|
||||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
// the admin has requested that the same user be displayed again so reload the page
|
|
||||||
// (e.g., if they changed their mind about changes they were making)
|
|
||||||
this.$router.go();
|
this.$router.go();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
|
import VueRouter from 'vue-router';
|
||||||
|
|
||||||
|
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
methods: {
|
methods: {
|
||||||
async saveHero ({ hero, msg = 'User', clearData }) {
|
async saveHero ({
|
||||||
|
hero,
|
||||||
|
msg = 'User',
|
||||||
|
clearData,
|
||||||
|
reloadData,
|
||||||
|
}) {
|
||||||
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
|
||||||
await this.$store.dispatch('snackbars:add', {
|
await this.$store.dispatch('snackbars:add', {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -14,6 +23,20 @@ export default {
|
|||||||
// The admin should re-fetch the data if they need to keep working on that user.
|
// The admin should re-fetch the data if they need to keep working on that user.
|
||||||
this.$emit('clear-data');
|
this.$emit('clear-data');
|
||||||
this.$router.push({ name: 'adminPanel' });
|
this.$router.push({ name: 'adminPanel' });
|
||||||
|
} else if (reloadData) {
|
||||||
|
if (this.$router.currentRoute.name === 'adminPanelUser') {
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'adminPanel',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.$router.push({
|
||||||
|
name: 'adminPanelUser',
|
||||||
|
params: { userIdentifier: hero._id },
|
||||||
|
}).catch(failure => {
|
||||||
|
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||||
|
this.$router.go();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
>
|
>
|
||||||
Could not find any matching users.
|
Could not find any matching users.
|
||||||
</div>
|
</div>
|
||||||
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
<loading-spinner
|
||||||
|
v-if="isSearching"
|
||||||
|
class="mx-auto mb-2"
|
||||||
|
dark-color="true"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="users.length > 0"
|
v-if="users.length > 0"
|
||||||
class="list-group"
|
class="list-group"
|
||||||
@@ -59,6 +63,10 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
LoadingSpinner,
|
LoadingSpinner,
|
||||||
},
|
},
|
||||||
|
beforeRouteUpdate (to, from, next) {
|
||||||
|
this.userIdentifier = to.params.userIdentifier;
|
||||||
|
next();
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
userIdentifier: '',
|
userIdentifier: '',
|
||||||
@@ -70,10 +78,6 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
...mapState({ user: 'user.data' }),
|
...mapState({ user: 'user.data' }),
|
||||||
},
|
},
|
||||||
beforeRouteUpdate (to, from, next) {
|
|
||||||
this.userIdentifier = to.params.userIdentifier;
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
userIdentifier () {
|
userIdentifier () {
|
||||||
this.isSearching = true;
|
this.isSearching = true;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<li
|
<li
|
||||||
v-for="item in achievements"
|
v-for="item in achievements"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
|
v-b-tooltip.hover="item.notes"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="saveItem(item)">
|
<form @submit.prevent="saveItem(item)">
|
||||||
<span
|
<span
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
</span>
|
</span>
|
||||||
:
|
:
|
||||||
{{ item.text || item.key }}
|
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
<li
|
<li
|
||||||
v-for="item in nestedAchievements[achievementType]"
|
v-for="item in nestedAchievements[achievementType]"
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
|
v-b-tooltip.hover="item.notes"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="saveItem(item)">
|
<form @submit.prevent="saveItem(item)">
|
||||||
<span
|
<span
|
||||||
@@ -78,7 +80,7 @@
|
|||||||
{{ item.value }}
|
{{ item.value }}
|
||||||
</span>
|
</span>
|
||||||
:
|
:
|
||||||
{{ item.text || item.key }}
|
{{ item.text || item.key }} - <i> {{ item.key }} </i>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -143,79 +145,28 @@ function getText (achievementItem) {
|
|||||||
}
|
}
|
||||||
const { titleKey } = achievementItem;
|
const { titleKey } = achievementItem;
|
||||||
if (titleKey !== undefined) {
|
if (titleKey !== undefined) {
|
||||||
return i18n.t(titleKey, 'en');
|
return i18n.t(titleKey);
|
||||||
}
|
}
|
||||||
const { singularTitleKey } = achievementItem;
|
const { singularTitleKey } = achievementItem;
|
||||||
if (singularTitleKey !== undefined) {
|
if (singularTitleKey !== undefined) {
|
||||||
return i18n.t(singularTitleKey, 'en');
|
return i18n.t(singularTitleKey);
|
||||||
}
|
}
|
||||||
return achievementItem.key;
|
return achievementItem.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function collateItemData (self) {
|
function getNotes (achievementItem, count) {
|
||||||
const achievements = [];
|
if (achievementItem === undefined) {
|
||||||
const nestedAchievements = {};
|
return '';
|
||||||
const basePath = 'achievements';
|
|
||||||
const ownedAchievements = self.hero.achievements;
|
|
||||||
const allAchievements = content.achievements;
|
|
||||||
|
|
||||||
for (const key of Object.keys(ownedAchievements)) {
|
|
||||||
const value = ownedAchievements[key];
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
nestedAchievements[key] = [];
|
|
||||||
for (const nestedKey of Object.keys(value)) {
|
|
||||||
const valueIsInteger = self.integerTypes.includes(key);
|
|
||||||
let text = nestedKey;
|
|
||||||
if (allAchievements[key] && allAchievements[key][nestedKey]) {
|
|
||||||
text = getText(allAchievements[key][nestedKey]);
|
|
||||||
}
|
|
||||||
nestedAchievements[key].push({
|
|
||||||
key: nestedKey,
|
|
||||||
text,
|
|
||||||
achievementType: key,
|
|
||||||
modified: false,
|
|
||||||
path: `${basePath}.${key}.${nestedKey}`,
|
|
||||||
value: value[nestedKey],
|
|
||||||
valueIsInteger,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const valueIsInteger = self.integerTypes.includes(key);
|
|
||||||
achievements.push({
|
|
||||||
key,
|
|
||||||
text: getText(allAchievements[key]),
|
|
||||||
modified: false,
|
|
||||||
path: `${basePath}.${key}`,
|
|
||||||
value: ownedAchievements[key],
|
|
||||||
valueIsInteger,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const { textKey } = achievementItem;
|
||||||
for (const key of Object.keys(allAchievements)) {
|
if (textKey !== undefined) {
|
||||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
return i18n.t(textKey, { count });
|
||||||
if (ownedAchievements[key] === undefined) {
|
|
||||||
const valueIsInteger = self.integerTypes.includes(key);
|
|
||||||
achievements.push({
|
|
||||||
key,
|
|
||||||
text: getText(allAchievements[key]),
|
|
||||||
modified: false,
|
|
||||||
path: `${basePath}.${key}`,
|
|
||||||
value: valueIsInteger ? 0 : false,
|
|
||||||
valueIsInteger,
|
|
||||||
neverOwned: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const { singularTextKey } = achievementItem;
|
||||||
self.achievements = achievements;
|
if (singularTextKey !== undefined) {
|
||||||
self.nestedAchievements = nestedAchievements;
|
return i18n.t(singularTextKey, { count });
|
||||||
}
|
}
|
||||||
|
return '';
|
||||||
function resetData (self) {
|
|
||||||
collateItemData(self);
|
|
||||||
self.nestedAchievementKeys.forEach(itemType => { self.expandItemType[itemType] = false; });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -241,26 +192,34 @@ export default {
|
|||||||
},
|
},
|
||||||
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
|
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
|
||||||
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
|
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
|
||||||
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests'],
|
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
|
||||||
|
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
|
||||||
|
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
|
||||||
|
],
|
||||||
|
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
|
||||||
|
'congrats', 'getwell'],
|
||||||
achievements: [],
|
achievements: [],
|
||||||
nestedAchievements: {},
|
nestedAchievements: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
resetCounter () {
|
resetCounter () {
|
||||||
resetData(this);
|
this.resetData();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
resetData(this);
|
this.resetData();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async saveItem (item) {
|
async saveItem (item) {
|
||||||
// prepare the item's new value and path for being saved
|
await this.saveHero({
|
||||||
this.hero.achievementPath = item.path;
|
hero: {
|
||||||
this.hero.achievementVal = item.value;
|
_id: this.hero._id,
|
||||||
|
achievementPath: item.path,
|
||||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
achievementVal: item.value,
|
||||||
|
},
|
||||||
|
msg: item.path,
|
||||||
|
});
|
||||||
item.modified = false;
|
item.modified = false;
|
||||||
},
|
},
|
||||||
enableValueChange (item) {
|
enableValueChange (item) {
|
||||||
@@ -270,6 +229,85 @@ export default {
|
|||||||
item.value = !item.value;
|
item.value = !item.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
resetData () {
|
||||||
|
this.collateItemData();
|
||||||
|
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
|
||||||
|
},
|
||||||
|
collateItemData () {
|
||||||
|
const achievements = [];
|
||||||
|
const nestedAchievements = {};
|
||||||
|
const basePath = 'achievements';
|
||||||
|
const ownedAchievements = this.hero.achievements;
|
||||||
|
const allAchievements = content.achievements;
|
||||||
|
|
||||||
|
const ownedKeys = Object.keys(ownedAchievements).sort();
|
||||||
|
for (const key of ownedKeys) {
|
||||||
|
const value = ownedAchievements[key];
|
||||||
|
let contentKey = key;
|
||||||
|
if (this.cardTypes.indexOf(key) !== -1) {
|
||||||
|
contentKey += 'Cards';
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
nestedAchievements[key] = [];
|
||||||
|
for (const nestedKey of Object.keys(value)) {
|
||||||
|
const valueIsInteger = this.integerTypes.includes(key);
|
||||||
|
let text = nestedKey;
|
||||||
|
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||||
|
text = getText(allAchievements[key][contentKey]);
|
||||||
|
}
|
||||||
|
let notes = '';
|
||||||
|
if (allAchievements[key] && allAchievements[key][contentKey]) {
|
||||||
|
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
|
||||||
|
}
|
||||||
|
nestedAchievements[key].push({
|
||||||
|
key: nestedKey,
|
||||||
|
text,
|
||||||
|
notes,
|
||||||
|
achievementType: key,
|
||||||
|
modified: false,
|
||||||
|
path: `${basePath}.${key}.${nestedKey}`,
|
||||||
|
value: value[nestedKey],
|
||||||
|
valueIsInteger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const valueIsInteger = this.integerTypes.includes(key);
|
||||||
|
achievements.push({
|
||||||
|
key,
|
||||||
|
text: getText(allAchievements[contentKey]),
|
||||||
|
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
|
||||||
|
modified: false,
|
||||||
|
path: `${basePath}.${key}`,
|
||||||
|
value: ownedAchievements[key],
|
||||||
|
valueIsInteger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allKeys = Object.keys(allAchievements).sort();
|
||||||
|
|
||||||
|
for (const key of allKeys) {
|
||||||
|
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
||||||
|
const ownedKey = key.replace('Cards', '');
|
||||||
|
if (ownedAchievements[ownedKey] === undefined) {
|
||||||
|
const valueIsInteger = this.integerTypes.includes(ownedKey);
|
||||||
|
achievements.push({
|
||||||
|
key: ownedKey,
|
||||||
|
text: getText(allAchievements[key]),
|
||||||
|
notes: getNotes(allAchievements[key], 0),
|
||||||
|
modified: false,
|
||||||
|
path: `${basePath}.${ownedKey}`,
|
||||||
|
value: valueIsInteger ? 0 : false,
|
||||||
|
valueIsInteger,
|
||||||
|
neverOwned: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.achievements = achievements;
|
||||||
|
this.nestedAchievements = nestedAchievements;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
<form
|
||||||
|
@submit.prevent="saveHero({ hero: {
|
||||||
|
_id: hero._id,
|
||||||
|
contributor: hero.contributor,
|
||||||
|
secret: hero.secret,
|
||||||
|
permissions: hero.permissions,
|
||||||
|
}, msg: 'Contributor details', clearData: true })"
|
||||||
|
>
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
@@ -8,6 +15,12 @@
|
|||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Contributor Details
|
Contributor Details
|
||||||
|
<b
|
||||||
|
v-if="hasUnsavedChanges && !expand"
|
||||||
|
class="text-warning float-right"
|
||||||
|
>
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -104,13 +117,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
class="card-footer"
|
class="card-footer d-flex align-items-center justify-content-between"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary mt-1"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -190,6 +206,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hasUnsavedChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
<form
|
||||||
|
@submit.prevent="saveHero({ hero: {
|
||||||
|
_id: hero._id,
|
||||||
|
auth: hero.auth,
|
||||||
|
preferences: hero.preferences,
|
||||||
|
}, msg: 'Authentication' })"
|
||||||
|
>
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
@@ -38,7 +44,10 @@
|
|||||||
<strong v-else>No</strong>
|
<strong v-else>No</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="cronError" class="form-group row">
|
<div
|
||||||
|
v-if="cronError"
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||||
<br>
|
<br>
|
||||||
@@ -53,12 +62,12 @@
|
|||||||
<div class="col-sm-9 col-form-label">
|
<div class="col-sm-9 col-form-label">
|
||||||
<strong>
|
<strong>
|
||||||
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||||
<button
|
<a
|
||||||
class="btn btn-warning btn-sm ml-4"
|
class="btn btn-warning btn-sm ml-4"
|
||||||
@click="resetCron()"
|
@click="resetCron()"
|
||||||
>
|
>
|
||||||
Reset Cron to Yesterday
|
Reset Cron to Yesterday
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
@@ -110,13 +119,14 @@
|
|||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-3 col-form-label">API Token</label>
|
<label class="col-sm-3 col-form-label">API Token</label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<button
|
<a
|
||||||
|
href="#"
|
||||||
value="Change API Token"
|
value="Change API Token"
|
||||||
class="btn btn-danger"
|
class="btn btn-danger"
|
||||||
@click="changeApiToken()"
|
@click="changeApiToken()"
|
||||||
>
|
>
|
||||||
Change API Token
|
Change API Token
|
||||||
</button>
|
</a>
|
||||||
<div
|
<div
|
||||||
v-if="tokenModified"
|
v-if="tokenModified"
|
||||||
>
|
>
|
||||||
@@ -268,13 +278,24 @@ export default {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
async changeApiToken () {
|
async changeApiToken () {
|
||||||
this.hero.changeApiToken = true;
|
await this.saveHero({
|
||||||
await this.saveHero({ hero: this.hero, msg: 'API Token' });
|
hero: {
|
||||||
|
_id: this.hero._id,
|
||||||
|
changeApiToken: true,
|
||||||
|
},
|
||||||
|
msg: 'API Token',
|
||||||
|
});
|
||||||
this.tokenModified = true;
|
this.tokenModified = true;
|
||||||
},
|
},
|
||||||
resetCron () {
|
resetCron () {
|
||||||
this.hero.resetCron = true;
|
this.saveHero({
|
||||||
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
|
hero: {
|
||||||
|
_id: this.hero._id,
|
||||||
|
resetCron: true,
|
||||||
|
},
|
||||||
|
msg: 'Last Cron',
|
||||||
|
clearData: true,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
:
|
:
|
||||||
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
|
||||||
</span>
|
</span>
|
||||||
{{ item.set }}
|
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="item.modified"
|
v-if="item.modified"
|
||||||
@@ -232,11 +232,14 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async saveItem (item) {
|
async saveItem (item) {
|
||||||
// prepare the item's new value and path for being saved
|
await this.saveHero({
|
||||||
this.hero.purchasedPath = item.path;
|
hero: {
|
||||||
this.hero.purchasedVal = item.value;
|
_id: this.hero._id,
|
||||||
|
purchasedPath: item.path,
|
||||||
await this.saveHero({ hero: this.hero, msg: item.path });
|
purchasedVal: item.value,
|
||||||
|
},
|
||||||
|
msg: item.path,
|
||||||
|
});
|
||||||
item.modified = false;
|
item.modified = false;
|
||||||
},
|
},
|
||||||
enableValueChange (item) {
|
enableValueChange (item) {
|
||||||
|
|||||||
@@ -15,10 +15,17 @@
|
|||||||
<privileges-and-gems
|
<privileges-and-gems
|
||||||
:hero="hero"
|
:hero="hero"
|
||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
|
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
|
||||||
|
[hero.auth, unModifiedHero.auth],
|
||||||
|
[hero.balance, unModifiedHero.balance],
|
||||||
|
[hero.secret, unModifiedHero.secret])"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<subscription-and-perks
|
<subscription-and-perks
|
||||||
:hero="hero"
|
:hero="hero"
|
||||||
|
:group-plans="groupPlans"
|
||||||
|
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
|
||||||
|
unModifiedHero.purchased.plan])"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<cron-and-auth
|
<cron-and-auth
|
||||||
@@ -29,6 +36,7 @@
|
|||||||
<user-profile
|
<user-profile
|
||||||
:hero="hero"
|
:hero="hero"
|
||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
|
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<party-and-quest
|
<party-and-quest
|
||||||
@@ -47,6 +55,12 @@
|
|||||||
:preferences="hero.preferences"
|
:preferences="hero.preferences"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<stats
|
||||||
|
:hero="hero"
|
||||||
|
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
|
||||||
|
:reset-counter="resetCounter"
|
||||||
|
/>
|
||||||
|
|
||||||
<items-owned
|
<items-owned
|
||||||
:hero="hero"
|
:hero="hero"
|
||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
@@ -67,8 +81,18 @@
|
|||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<user-history
|
||||||
|
:hero="hero"
|
||||||
|
:reset-counter="resetCounter"
|
||||||
|
/>
|
||||||
|
|
||||||
<contributor-details
|
<contributor-details
|
||||||
:hero="hero"
|
:hero="hero"
|
||||||
|
:hasUnsavedChanges="hasUnsavedChanges(
|
||||||
|
[hero.contributor, unModifiedHero.contributor],
|
||||||
|
[hero.permissions, unModifiedHero.permissions],
|
||||||
|
[hero.secret, unModifiedHero.secret],
|
||||||
|
)"
|
||||||
:reset-counter="resetCounter"
|
:reset-counter="resetCounter"
|
||||||
@clear-data="clearData"
|
@clear-data="clearData"
|
||||||
/>
|
/>
|
||||||
@@ -109,6 +133,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import isEqualWith from 'lodash/isEqualWith';
|
||||||
import BasicDetails from './basicDetails';
|
import BasicDetails from './basicDetails';
|
||||||
import ItemsOwned from './itemsOwned';
|
import ItemsOwned from './itemsOwned';
|
||||||
import CronAndAuth from './cronAndAuth';
|
import CronAndAuth from './cronAndAuth';
|
||||||
@@ -121,6 +146,8 @@ import Transactions from './transactions';
|
|||||||
import SubscriptionAndPerks from './subscriptionAndPerks';
|
import SubscriptionAndPerks from './subscriptionAndPerks';
|
||||||
import CustomizationsOwned from './customizationsOwned.vue';
|
import CustomizationsOwned from './customizationsOwned.vue';
|
||||||
import Achievements from './achievements.vue';
|
import Achievements from './achievements.vue';
|
||||||
|
import UserHistory from './userHistory.vue';
|
||||||
|
import Stats from './stats.vue';
|
||||||
|
|
||||||
import { userStateMixin } from '../../../mixins/userState';
|
import { userStateMixin } from '../../../mixins/userState';
|
||||||
|
|
||||||
@@ -135,6 +162,8 @@ export default {
|
|||||||
PrivilegesAndGems,
|
PrivilegesAndGems,
|
||||||
ContributorDetails,
|
ContributorDetails,
|
||||||
Transactions,
|
Transactions,
|
||||||
|
UserHistory,
|
||||||
|
Stats,
|
||||||
SubscriptionAndPerks,
|
SubscriptionAndPerks,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
Achievements,
|
Achievements,
|
||||||
@@ -148,8 +177,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
userIdentifier: '',
|
userIdentifier: '',
|
||||||
resetCounter: 0,
|
resetCounter: 0,
|
||||||
|
unModifiedHero: {},
|
||||||
hero: {},
|
hero: {},
|
||||||
party: {},
|
party: {},
|
||||||
|
groupPlans: [],
|
||||||
hasParty: false,
|
hasParty: false,
|
||||||
partyNotExistError: false,
|
partyNotExistError: false,
|
||||||
adminHasPrivForParty: true,
|
adminHasPrivForParty: true,
|
||||||
@@ -168,6 +199,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearData () {
|
clearData () {
|
||||||
|
this.unModifiedHero = {};
|
||||||
this.hero = {};
|
this.hero = {};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -176,6 +208,7 @@ export default {
|
|||||||
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
|
||||||
|
|
||||||
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
|
||||||
|
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
|
||||||
|
|
||||||
if (!this.hero.flags) {
|
if (!this.hero.flags) {
|
||||||
this.hero.flags = {
|
this.hero.flags = {
|
||||||
@@ -206,8 +239,38 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
|
||||||
|
try {
|
||||||
|
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
|
||||||
|
} catch (e) {
|
||||||
|
this.groupPlans = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||||
},
|
},
|
||||||
|
hasUnsavedChanges (...comparisons) {
|
||||||
|
for (const index in comparisons) {
|
||||||
|
if (index && comparisons[index]) {
|
||||||
|
const objs = comparisons[index];
|
||||||
|
const obj1 = objs[0];
|
||||||
|
const obj2 = objs[1];
|
||||||
|
if (!isEqualWith(obj1, obj2, (x, y) => {
|
||||||
|
if (typeof x === 'object' && typeof y === 'object') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (x === false && y === undefined) {
|
||||||
|
// Special case for checkboxes
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return x == y; // eslint-disable-line eqeqeq
|
||||||
|
})) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -269,16 +269,19 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async saveItem (item) {
|
async saveItem (item) {
|
||||||
// prepare the item's new value and path for being saved
|
// prepare the item's new value and path for being saved
|
||||||
this.hero.itemPath = item.path;
|
const toSave = {
|
||||||
|
_id: this.hero._id,
|
||||||
|
};
|
||||||
|
toSave.itemPath = item.path;
|
||||||
if (item.value === null) {
|
if (item.value === null) {
|
||||||
this.hero.itemVal = 'null';
|
toSave.itemVal = 'null';
|
||||||
} else if (item.value === false) {
|
} else if (item.value === false) {
|
||||||
this.hero.itemVal = 'false';
|
toSave.itemVal = 'false';
|
||||||
} else {
|
} else {
|
||||||
this.hero.itemVal = item.value;
|
toSave.itemVal = item.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.saveHero({ hero: this.hero, msg: item.key });
|
await this.saveHero({ hero: toSave, msg: item.key });
|
||||||
item.neverOwned = false;
|
item.neverOwned = false;
|
||||||
item.modified = false;
|
item.modified = false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,22 +31,41 @@
|
|||||||
v-html="questErrors"
|
v-html="questErrors"
|
||||||
></p>
|
></p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="userHasParty">
|
||||||
<div>
|
<div class="form-group row">
|
||||||
Party:
|
<label class="col-sm-3 col-form-label">
|
||||||
<span v-if="userHasParty">
|
Party ID
|
||||||
yes: party ID {{ groupPartyData._id }},
|
</label>
|
||||||
member count {{ groupPartyData.memberCount }} (may be wrong)
|
<strong class="col-sm-9 col-form-label">
|
||||||
<br>
|
{{ groupPartyData._id }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Estimated Member Count
|
||||||
|
</label>
|
||||||
|
<strong class="col-sm-9 col-form-label">
|
||||||
|
{{ groupPartyData.memberCount }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Leader
|
||||||
|
</label>
|
||||||
|
<strong class="col-sm-9 col-form-label">
|
||||||
<span v-if="userIsPartyLeader">User is the party leader</span>
|
<span v-if="userIsPartyLeader">User is the party leader</span>
|
||||||
<span v-else>Party leader is
|
<span v-else>Party leader is
|
||||||
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
|
||||||
{{ groupPartyData.leader }}
|
{{ groupPartyData.leader }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</strong>
|
||||||
<span v-else>no</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="btn btn-danger"
|
||||||
|
@click="removeFromParty()">Remove from Party</div>
|
||||||
|
</div>
|
||||||
|
<strong v-else>User is not in a party.</strong>
|
||||||
<div class="subsection-start">
|
<div class="subsection-start">
|
||||||
<p v-html="questStatus"></p>
|
<p v-html="questStatus"></p>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,6 +75,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as quests from '@/../../common/script/content/quests';
|
import * as quests from '@/../../common/script/content/quests';
|
||||||
|
import saveHero from '../mixins/saveHero';
|
||||||
|
|
||||||
function determineQuestStatus (self) {
|
function determineQuestStatus (self) {
|
||||||
// Quest data is in the user doc and party doc. They can be out of sync.
|
// Quest data is in the user doc and party doc. They can be out of sync.
|
||||||
@@ -271,6 +291,7 @@ function resetData (self) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
mixins: [saveHero],
|
||||||
props: {
|
props: {
|
||||||
resetCounter: {
|
resetCounter: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -318,5 +339,14 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
resetData(this);
|
resetData(this);
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
removeFromParty () {
|
||||||
|
this.saveHero({
|
||||||
|
hero: { _id: this.userId, removeFromParty: true },
|
||||||
|
msg: 'Removed from party',
|
||||||
|
reloadData: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
<form @submit.prevent="saveHero({hero: {
|
||||||
|
_id: hero._id,
|
||||||
|
flags: hero.flags,
|
||||||
|
balance: hero.balance,
|
||||||
|
auth: hero.auth,
|
||||||
|
secret: hero.secret,
|
||||||
|
}, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
@@ -8,6 +14,9 @@
|
|||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
Privileges, Gem Balance
|
Privileges, Gem Balance
|
||||||
|
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -117,13 +126,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
class="card-footer"
|
class="card-footer d-flex align-items-center justify-content-between"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary mt-1"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -169,6 +181,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hasUnsavedChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label
|
||||||
|
class="col-sm-3 col-form-label"
|
||||||
|
:class="color"
|
||||||
|
>{{ label }}</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
:value="value"
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
:step="step"
|
||||||
|
:max="max"
|
||||||
|
:min="min"
|
||||||
|
@input="$emit('input', parseInt($event.target.value, 10))"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.about-row {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-label {
|
||||||
|
color: $red_100;
|
||||||
|
}
|
||||||
|
.blue-label {
|
||||||
|
color: $blue_100;
|
||||||
|
}
|
||||||
|
.purple-label {
|
||||||
|
color: $purple_300;
|
||||||
|
}
|
||||||
|
.yellow-label {
|
||||||
|
color: $yellow_50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'value',
|
||||||
|
event: 'input',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'text-label',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: String,
|
||||||
|
default: 'any',
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
286
website/client/src/components/admin-panel/user-support/stats.vue
Normal file
286
website/client/src/components/admin-panel/user-support/stats.vue
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submitClicked()">
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3
|
||||||
|
class="mb-0 mt-0"
|
||||||
|
:class="{'open': expand}"
|
||||||
|
@click="expand = !expand"
|
||||||
|
>
|
||||||
|
Stats
|
||||||
|
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
|
<stats-row
|
||||||
|
label="Health"
|
||||||
|
color="red-label"
|
||||||
|
:max="maxHealth"
|
||||||
|
v-model="hero.stats.hp" />
|
||||||
|
<stats-row
|
||||||
|
label="Experience"
|
||||||
|
color="yellow-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxFieldHardCap"
|
||||||
|
v-model="hero.stats.exp" />
|
||||||
|
<stats-row
|
||||||
|
label="Mana"
|
||||||
|
color="blue-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxFieldHardCap"
|
||||||
|
v-model="hero.stats.mp" />
|
||||||
|
<stats-row
|
||||||
|
label="Level"
|
||||||
|
step="1"
|
||||||
|
min="0"
|
||||||
|
:max="maxLevelHardCap"
|
||||||
|
v-model="hero.stats.lvl" />
|
||||||
|
<stats-row
|
||||||
|
label="Gold"
|
||||||
|
min="0"
|
||||||
|
:max="maxFieldHardCap"
|
||||||
|
v-model="hero.stats.gp" />
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">Selected Class</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select
|
||||||
|
id="selectedClass"
|
||||||
|
v-model="hero.stats.class"
|
||||||
|
class="form-control"
|
||||||
|
:disabled="hero.stats.lvl < 10"
|
||||||
|
>
|
||||||
|
<option value="warrior">Warrior</option>
|
||||||
|
<option value="wizard">Mage</option>
|
||||||
|
<option value="healer">Healer</option>
|
||||||
|
<option value="rogue">Rogue</option>
|
||||||
|
</select>
|
||||||
|
<small>
|
||||||
|
When changing class, players usually need stat points deallocated as well.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Stat Points</h3>
|
||||||
|
<stats-row
|
||||||
|
label="Unallocated"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
:max="maxStatPoints"
|
||||||
|
v-model="hero.stats.points" />
|
||||||
|
<stats-row
|
||||||
|
label="Strength"
|
||||||
|
color="red-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxStatPoints"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.str" />
|
||||||
|
<stats-row
|
||||||
|
label="Intelligence"
|
||||||
|
color="blue-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxStatPoints"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.int" />
|
||||||
|
<stats-row
|
||||||
|
label="Perception"
|
||||||
|
color="purple-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxStatPoints"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.per" />
|
||||||
|
<stats-row
|
||||||
|
label="Constitution"
|
||||||
|
color="yellow-label"
|
||||||
|
min="0"
|
||||||
|
:max="maxStatPoints"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.con" />
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="offset-sm-3 col-sm-9">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning btn-sm"
|
||||||
|
@click="deallocateStatPoints">
|
||||||
|
Deallocate all stat points
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row" v-if="statPointsIncorrect">
|
||||||
|
<div class="offset-sm-3 col-sm-9 text-danger">
|
||||||
|
Error: Sum of stat points should equal the users level
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Buffs</h3>
|
||||||
|
<stats-row
|
||||||
|
label="Strength"
|
||||||
|
color="red-label"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.buffs.str" />
|
||||||
|
<stats-row
|
||||||
|
label="Intelligence"
|
||||||
|
color="blue-label"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.buffs.int" />
|
||||||
|
<stats-row
|
||||||
|
label="Perception"
|
||||||
|
color="purple-label"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.buffs.per" />
|
||||||
|
<stats-row
|
||||||
|
label="Constitution"
|
||||||
|
color="yellow-label"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
v-model="hero.stats.buffs.con" />
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="offset-sm-3 col-sm-9">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-warning btn-sm"
|
||||||
|
@click="resetBuffs">
|
||||||
|
Reset Buffs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-footer d-flex align-items-center justify-content-between"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Save"
|
||||||
|
class="btn btn-primary mt-1"
|
||||||
|
>
|
||||||
|
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.about-row {
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
MAX_HEALTH,
|
||||||
|
MAX_STAT_POINTS,
|
||||||
|
MAX_LEVEL_HARD_CAP,
|
||||||
|
MAX_FIELD_HARD_CAP,
|
||||||
|
} from '@/../../common/script/constants';
|
||||||
|
import markdownDirective from '@/directives/markdown';
|
||||||
|
import saveHero from '../mixins/saveHero';
|
||||||
|
|
||||||
|
import { mapState } from '@/libs/store';
|
||||||
|
import { userStateMixin } from '../../../mixins/userState';
|
||||||
|
|
||||||
|
import StatsRow from './stats-row';
|
||||||
|
|
||||||
|
function resetData (self) {
|
||||||
|
self.expand = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
directives: {
|
||||||
|
markdown: markdownDirective,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
StatsRow,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
userStateMixin,
|
||||||
|
saveHero,
|
||||||
|
],
|
||||||
|
computed: {
|
||||||
|
...mapState({ user: 'user.data' }),
|
||||||
|
statPointsIncorrect () {
|
||||||
|
if (this.hero.stats.lvl >= 10) {
|
||||||
|
return (parseInt(this.hero.stats.points, 10)
|
||||||
|
+ parseInt(this.hero.stats.str, 10)
|
||||||
|
+ parseInt(this.hero.stats.int, 10)
|
||||||
|
+ parseInt(this.hero.stats.per, 10)
|
||||||
|
+ parseInt(this.hero.stats.con, 10)
|
||||||
|
) !== this.hero.stats.lvl;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
resetCounter: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hasUnsavedChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
maxHealth: MAX_HEALTH,
|
||||||
|
maxStatPoints: MAX_STAT_POINTS,
|
||||||
|
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
|
||||||
|
maxFieldHardCap: MAX_FIELD_HARD_CAP,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
resetCounter () {
|
||||||
|
resetData(this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
resetData(this);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitClicked () {
|
||||||
|
if (this.statPointsIncorrect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saveHero({
|
||||||
|
hero: {
|
||||||
|
_id: this.hero._id,
|
||||||
|
stats: this.hero.stats,
|
||||||
|
},
|
||||||
|
msg: 'Stats',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetBuffs () {
|
||||||
|
this.hero.stats.buffs = {
|
||||||
|
str: 0,
|
||||||
|
int: 0,
|
||||||
|
per: 0,
|
||||||
|
con: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
deallocateStatPoints () {
|
||||||
|
this.hero.stats.points = this.hero.stats.lvl;
|
||||||
|
this.hero.stats.str = 0;
|
||||||
|
this.hero.stats.int = 0;
|
||||||
|
this.hero.stats.per = 0;
|
||||||
|
this.hero.stats.con = 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,30 +1,135 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
<form
|
||||||
|
@submit.prevent="saveHero({ hero: {
|
||||||
|
_id: hero._id,
|
||||||
|
purchased: hero.purchased
|
||||||
|
}, msg: 'Subscription Perks' })"
|
||||||
|
>
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<div class="card-header">
|
<div class="card-header"
|
||||||
|
@click="expand = !expand">
|
||||||
<h3
|
<h3
|
||||||
class="mb-0 mt-0"
|
class="mb-0 mt-0"
|
||||||
:class="{ 'open': expand }"
|
:class="{ 'open': expand }"
|
||||||
@click="expand = !expand"
|
|
||||||
>
|
>
|
||||||
Subscription, Monthly Perks
|
Subscription, Monthly Perks
|
||||||
|
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
class="card-body"
|
class="card-body"
|
||||||
>
|
>
|
||||||
<div v-if="hero.purchased.plan.paymentMethod">
|
<div
|
||||||
Payment method:
|
class="form-group row"
|
||||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
>
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Payment method:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input v-model="hero.purchased.plan.paymentMethod"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
v-if="!isRegularPaymentMethod"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-else
|
||||||
|
v-model="hero.purchased.plan.paymentMethod"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<option value="groupPlan">Group Plan</option>
|
||||||
|
<option value="Stripe">Stripe</option>
|
||||||
|
<option value="Apple">Apple</option>
|
||||||
|
<option value="Google">Google</option>
|
||||||
|
<option value="Amazon Payments">Amazon</option>
|
||||||
|
<option value="PayPal">PayPal</option>
|
||||||
|
<option value="Gift">Gift</option>
|
||||||
|
<option value="">Clear out</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hero.purchased.plan.planId">
|
<div
|
||||||
Payment schedule ("basic-earned" is monthly):
|
class="form-group row"
|
||||||
<strong>{{ hero.purchased.plan.planId }}</strong>
|
>
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Payment schedule:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input v-model="hero.purchased.plan.planId"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
v-if="!isRegularPlanId"
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
v-else
|
||||||
|
v-model="hero.purchased.plan.planId"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
<option value="basic_earned">Monthly recurring</option>
|
||||||
|
<option value="basic_3mo">3 Months recurring</option>
|
||||||
|
<option value="basic_6mo">6 Months recurring</option>
|
||||||
|
<option value="basic_12mo">12 Months recurring</option>
|
||||||
|
<option value="group_monthly">Group Plan (legacy)</option>
|
||||||
|
<option value="group_plan_auto">Group Plan (auto)</option>
|
||||||
|
<option value="">Clear out</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
|
<div
|
||||||
Group plan ID:
|
class="form-group row"
|
||||||
<strong>{{ hero.purchased.plan.owner }}</strong>
|
>
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Customer ID:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
v-model="hero.purchased.plan.customerId"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row"
|
||||||
|
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Group Plan Memberships:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-form-label">
|
||||||
|
<loading-spinner
|
||||||
|
v-if="!groupPlans"
|
||||||
|
dark-color=true
|
||||||
|
/>
|
||||||
|
<b
|
||||||
|
v-else-if="groupPlans.length === 0"
|
||||||
|
class="text-danger col-form-label"
|
||||||
|
>User is not part of an active group plan!</b>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
v-for="group in groupPlans"
|
||||||
|
:key="group._id"
|
||||||
|
class="card mb-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<h6 class="card-title">{{ group.name }}
|
||||||
|
<small class="float-right">{{ group._id }}</small>
|
||||||
|
</h6>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Leader: </strong>
|
||||||
|
<a
|
||||||
|
v-if="group.leader !== hero._id"
|
||||||
|
@click="switchUser(group.leader)"
|
||||||
|
>{{ group.leader }}</a>
|
||||||
|
<strong v-else class="text-success">This user</strong>
|
||||||
|
</p>
|
||||||
|
<p class="card-text">
|
||||||
|
<strong>Members: </strong> {{ group.memberCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="hero.purchased.plan.dateCreated"
|
v-if="hero.purchased.plan.dateCreated"
|
||||||
@@ -85,8 +190,18 @@
|
|||||||
<strong class="input-group-text">
|
<strong class="input-group-text">
|
||||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||||
</strong>
|
</strong>
|
||||||
|
<a class="btn btn-danger"
|
||||||
|
href="#"
|
||||||
|
v-b-modal.sub_termination_modal
|
||||||
|
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
|
||||||
|
Terminate
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<small v-if="!hero.purchased.plan.dateTerminated
|
||||||
|
&& hero.purchased.plan.planId" class="text-success">
|
||||||
|
The subscription does not have a termination date and is active.
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
@@ -101,6 +216,35 @@
|
|||||||
min="0"
|
min="0"
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
|
<small class="text-secondary">
|
||||||
|
Cumulative subscribed months across subscription periods.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Extra months:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
v-model="hero.purchased.plan.extraMonths"
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
step="any"
|
||||||
|
>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a class="btn btn-warning"
|
||||||
|
@click="applyExtraMonths"
|
||||||
|
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
|
||||||
|
Apply Credit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="text-secondary">
|
||||||
|
Additional credit that is applied if a subscription is cancelled.
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
@@ -174,10 +318,6 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hero.purchased.plan.extraMonths > 0">
|
|
||||||
Additional credit (applied upon cancellation):
|
|
||||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="form-group row">
|
<div class="form-group row">
|
||||||
<label class="col-sm-3 col-form-label">
|
<label class="col-sm-3 col-form-label">
|
||||||
Mystery Items:
|
Mystery Items:
|
||||||
@@ -199,23 +339,69 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group row"
|
||||||
|
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
|
||||||
|
<div class="offset-sm-3 col-sm-9">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary btn-sm"
|
||||||
|
@click="beginGroupPlanConvert">
|
||||||
|
Begin converting to group plan subscription
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group row"
|
||||||
|
v-if="isConvertingToGroupPlan">
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
|
Group Plan group ID:
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
v-model="groupPlanID"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
class="card-footer"
|
class="card-footer d-flex align-items-center justify-content-between"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary mt-1"
|
class="btn btn-primary mt-1"
|
||||||
|
@click="saveClicked"
|
||||||
>
|
>
|
||||||
|
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<b-modal id="sub_termination_modal" title="Set Termination Date">
|
||||||
|
<p>
|
||||||
|
You can set the sub benefit termination date to today or to the last
|
||||||
|
day of the current billing cycle. Any extra subscription credit will
|
||||||
|
then be processed and automatically added onto the selected date.
|
||||||
|
</p>
|
||||||
|
<template #modal-footer>
|
||||||
|
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
|
||||||
|
Close
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
|
||||||
|
Set to Today
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
|
||||||
|
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-modal>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.input-group-append {
|
.input-group-append {
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -231,21 +417,38 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import isUUID from 'validator/es/lib/isUUID';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { getPlanContext } from '@/../../common/script/cron';
|
import { getPlanContext } from '@/../../common/script/cron';
|
||||||
import saveHero from '../mixins/saveHero';
|
import saveHero from '../mixins/saveHero';
|
||||||
|
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
|
||||||
|
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [saveHero],
|
mixins: [saveHero],
|
||||||
|
components: {
|
||||||
|
LoadingSpinner,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
hero: {
|
hero: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hasUnsavedChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
groupPlans: {
|
||||||
|
type: Array,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
expand: false,
|
expand: false,
|
||||||
|
isConvertingToGroupPlan: false,
|
||||||
|
groupPlanID: '',
|
||||||
|
subscriptionBlocks,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -255,6 +458,30 @@ export default {
|
|||||||
if (!currentPlanContext.nextHourglassDate) return 'N/A';
|
if (!currentPlanContext.nextHourglassDate) return 'N/A';
|
||||||
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||||
},
|
},
|
||||||
|
isRegularPlanId () {
|
||||||
|
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
|
||||||
|
},
|
||||||
|
isRegularPaymentMethod () {
|
||||||
|
return [
|
||||||
|
'groupPlan',
|
||||||
|
'Group Plan',
|
||||||
|
'Stripe',
|
||||||
|
'Apple',
|
||||||
|
'Google',
|
||||||
|
'Amazon Payments',
|
||||||
|
'PayPal',
|
||||||
|
'Gift',
|
||||||
|
].includes(this.hero.purchased.plan.paymentMethod);
|
||||||
|
},
|
||||||
|
todayWithRemainingCycle () {
|
||||||
|
const now = moment();
|
||||||
|
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
|
||||||
|
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
|
||||||
|
while (terminationDate.isBefore(now)) {
|
||||||
|
terminationDate.add(monthCount, 'months');
|
||||||
|
}
|
||||||
|
return terminationDate;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
dateFormat (date) {
|
dateFormat (date) {
|
||||||
@@ -263,6 +490,46 @@ export default {
|
|||||||
}
|
}
|
||||||
return moment(date).format('YYYY/MM/DD');
|
return moment(date).format('YYYY/MM/DD');
|
||||||
},
|
},
|
||||||
|
terminateSubscription (terminationDate) {
|
||||||
|
if (terminationDate) {
|
||||||
|
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
|
||||||
|
} else {
|
||||||
|
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
|
||||||
|
}
|
||||||
|
this.applyExtraMonths();
|
||||||
|
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
|
||||||
|
},
|
||||||
|
applyExtraMonths () {
|
||||||
|
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
|
||||||
|
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
|
||||||
|
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
|
||||||
|
const extraDays = Math.ceil(30.5 * extraMonths);
|
||||||
|
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
|
||||||
|
this.hero.purchased.plan.extraMonths = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beginGroupPlanConvert () {
|
||||||
|
this.isConvertingToGroupPlan = true;
|
||||||
|
this.hero.purchased.plan.owner = '';
|
||||||
|
},
|
||||||
|
saveClicked (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.isConvertingToGroupPlan) {
|
||||||
|
if (!isUUID(this.groupPlanID)) {
|
||||||
|
alert('Invalid group ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
|
||||||
|
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
|
||||||
|
} else {
|
||||||
|
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
switchUser (id) {
|
||||||
|
if (window.confirm('Switch to this user?')) {
|
||||||
|
this.$emit('changeUserIdentifier', id);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mt-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3
|
||||||
|
class="mb-0 mt-0"
|
||||||
|
:class="{'open': expand}"
|
||||||
|
@click="toggleHistoryOpen"
|
||||||
|
>
|
||||||
|
User History
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expand"
|
||||||
|
class="card-body"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="clearfix">
|
||||||
|
<div class="mb-4 float-left">
|
||||||
|
<button
|
||||||
|
class="page-header btn-flat tab-button textCondensed"
|
||||||
|
:class="{'active': selectedTab === 'armoire'}"
|
||||||
|
@click="selectTab('armoire')"
|
||||||
|
>
|
||||||
|
Armoire
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="page-header btn-flat tab-button textCondensed"
|
||||||
|
:class="{'active': selectedTab === 'questInvites'}"
|
||||||
|
@click="selectTab('questInvites')"
|
||||||
|
>
|
||||||
|
Quest Invitations
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="page-header btn-flat tab-button textCondensed"
|
||||||
|
:class="{'active': selectedTab === 'cron'}"
|
||||||
|
@click="selectTab('cron')"
|
||||||
|
>
|
||||||
|
Cron
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div
|
||||||
|
v-if="selectedTab === 'armoire'"
|
||||||
|
class="col-12"
|
||||||
|
>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('timestamp') }}
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Client
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
Received
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="entry in armoire"
|
||||||
|
:key="entry.timestamp"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-b-tooltip.hover="entry.timestamp"
|
||||||
|
>{{ entry.timestamp | timeAgo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.client }}</td>
|
||||||
|
<td>{{ entry.reward }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="selectedTab === 'questInvites'"
|
||||||
|
class="col-12"
|
||||||
|
>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('timestamp') }}
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Client
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Quest Key
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Response
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="entry in questInviteResponses"
|
||||||
|
:key="entry.timestamp"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-b-tooltip.hover="entry.timestamp"
|
||||||
|
>{{ entry.timestamp | timeAgo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.client }}</td>
|
||||||
|
<td>{{ entry.quest }}</td>
|
||||||
|
<td>{{ questInviteResponseText(entry.response) }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="selectedTab === 'cron'"
|
||||||
|
class="col-12"
|
||||||
|
>
|
||||||
|
<table class="table">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
v-once
|
||||||
|
>
|
||||||
|
{{ $t('timestamp') }}
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Client
|
||||||
|
</th>
|
||||||
|
<th v-once>
|
||||||
|
Checkin Count
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="entry in cron"
|
||||||
|
:key="entry.timestamp"
|
||||||
|
>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-b-tooltip.hover="entry.timestamp"
|
||||||
|
>{{ entry.timestamp | timeAgo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ entry.client }}</td>
|
||||||
|
<td>{{ entry.checkinCount }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.page-header.btn-flat {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
height: 2rem;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-stretch: condensed;
|
||||||
|
line-height: 1.33;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: $gray-10;
|
||||||
|
|
||||||
|
margin-right: 1.125rem;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-bottom: 2.5rem;
|
||||||
|
|
||||||
|
&.active, &:hover {
|
||||||
|
color: $purple-300;
|
||||||
|
box-shadow: 0px -0.25rem 0px $purple-300 inset;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import moment from 'moment';
|
||||||
|
import { userStateMixin } from '../../../mixins/userState';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
filters: {
|
||||||
|
timeAgo (value) {
|
||||||
|
return moment(value).fromNow();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mixins: [userStateMixin],
|
||||||
|
props: {
|
||||||
|
hero: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
resetCounter: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
expand: false,
|
||||||
|
selectedTab: 'armoire',
|
||||||
|
armoire: [],
|
||||||
|
questInviteResponses: [],
|
||||||
|
cron: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
resetCounter () {
|
||||||
|
if (this.expand) {
|
||||||
|
this.retrieveUserHistory();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectTab (type) {
|
||||||
|
this.selectedTab = type;
|
||||||
|
},
|
||||||
|
async toggleHistoryOpen () {
|
||||||
|
this.expand = !this.expand;
|
||||||
|
if (this.expand) {
|
||||||
|
this.retrieveUserHistory();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async retrieveUserHistory () {
|
||||||
|
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||||
|
this.armoire = history.armoire;
|
||||||
|
this.questInviteResponses = history.questInviteResponses;
|
||||||
|
this.cron = history.cron;
|
||||||
|
},
|
||||||
|
questInviteResponseText (response) {
|
||||||
|
if (response === 'accept') {
|
||||||
|
return 'Accepted';
|
||||||
|
}
|
||||||
|
if (response === 'reject') {
|
||||||
|
return 'Rejected';
|
||||||
|
}
|
||||||
|
if (response === 'leave') {
|
||||||
|
return 'Left active quest';
|
||||||
|
}
|
||||||
|
if (response === 'invite') {
|
||||||
|
return 'Accepted as owner';
|
||||||
|
}
|
||||||
|
if (response === 'abort') {
|
||||||
|
return 'Aborted by owner';
|
||||||
|
}
|
||||||
|
if (response === 'abortByLeader') {
|
||||||
|
return 'Aborted by party leader';
|
||||||
|
}
|
||||||
|
if (response === 'cancel') {
|
||||||
|
return 'Cancelled before start';
|
||||||
|
}
|
||||||
|
if (response === 'cancelByLeader') {
|
||||||
|
return 'Cancelled before start by party leader';
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
<form
|
||||||
|
@submit.prevent="saveHero({hero: {
|
||||||
|
_id: hero._id,
|
||||||
|
profile: hero.profile
|
||||||
|
}, msg: 'Users Profile'})"
|
||||||
|
>
|
||||||
<div class="card mt-2">
|
<div class="card mt-2">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3
|
<h3
|
||||||
@@ -8,6 +13,9 @@
|
|||||||
@click="expand = !expand"
|
@click="expand = !expand"
|
||||||
>
|
>
|
||||||
User Profile
|
User Profile
|
||||||
|
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -51,13 +59,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="expand"
|
v-if="expand"
|
||||||
class="card-footer"
|
class="card-footer d-flex align-items-center justify-content-between"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Save"
|
value="Save"
|
||||||
class="btn btn-primary mt-1"
|
class="btn btn-primary mt-1"
|
||||||
>
|
>
|
||||||
|
<b v-if="hasUnsavedChanges" class="text-warning float-right">
|
||||||
|
Unsaved changes
|
||||||
|
</b>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -101,6 +112,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
hasUnsavedChanges: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
<h3>{{ $t('footerCompany') }}</h3>
|
<h3>{{ $t('footerCompany') }}</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<router-link to="/static/contact">
|
<a href="mailto:admin@habitica.com">
|
||||||
{{ $t('contactUs') }}
|
{{ $t('contactUs') }}
|
||||||
</router-link>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<router-link to="/static/press-kit">
|
<router-link to="/static/press-kit">
|
||||||
@@ -55,9 +55,9 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://habitica.fandom.com/wiki/Whats_New"
|
@click="showBailey()"
|
||||||
target="_blank"
|
>
|
||||||
>{{ $t('oldNews') }}
|
{{ $t('oldNews') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
|
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{{ $t('companyContribute') }}
|
>{{ $t('companyContribute') }}
|
||||||
</a>
|
</a>
|
||||||
@@ -158,13 +158,6 @@
|
|||||||
>{{ $t('guidanceForBlacksmiths') }}
|
>{{ $t('guidanceForBlacksmiths') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations"
|
|
||||||
target="_blank"
|
|
||||||
>{{ $t('communityExtensions') }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -205,12 +198,12 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
class="social-circle"
|
class="social-circle"
|
||||||
href="https://twitter.com/habitica/"
|
href="https://bsky.app/profile/habitica.com"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="social-icon svg-icon twitter"
|
class="social-icon svg-icon bluesky"
|
||||||
v-html="icons.twitter"
|
v-html="icons.bluesky"
|
||||||
></div>
|
></div>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -410,7 +403,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
.footer-row {
|
.footer-row {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
@@ -518,7 +511,7 @@ footer {
|
|||||||
background-color: $gray-500;
|
background-color: $gray-500;
|
||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
padding: 32px 142px 40px;
|
padding: 32px 142px 40px;
|
||||||
a {
|
a, a:not([href]) {
|
||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
@@ -807,7 +800,7 @@ h3 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.twitter svg {
|
.bluesky svg {
|
||||||
background-color: #e1e0e3;
|
background-color: #e1e0e3;
|
||||||
fill: #878190;
|
fill: #878190;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -845,12 +838,12 @@ import moment from 'moment';
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
// images
|
// images
|
||||||
import melior from '@/assets/svg/melior.svg';
|
import melior from '@/assets/svg/melior.svg?raw';
|
||||||
import twitter from '@/assets/svg/twitter.svg';
|
import bluesky from '@/assets/svg/bluesky.svg?raw';
|
||||||
import facebook from '@/assets/svg/facebook.svg';
|
import facebook from '@/assets/svg/facebook.svg?raw';
|
||||||
import instagram from '@/assets/svg/instagram.svg';
|
import instagram from '@/assets/svg/instagram.svg?raw';
|
||||||
import tumblr from '@/assets/svg/tumblr.svg';
|
import tumblr from '@/assets/svg/tumblr.svg?raw';
|
||||||
import heart from '@/assets/svg/heart.svg';
|
import heart from '@/assets/svg/heart.svg?raw';
|
||||||
|
|
||||||
// components & modals
|
// components & modals
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
@@ -858,12 +851,14 @@ import buyGemsModal from './payments/buyGemsModal.vue';
|
|||||||
import reportBug from '@/mixins/reportBug.js';
|
import reportBug from '@/mixins/reportBug.js';
|
||||||
import { worldStateMixin } from '@/mixins/worldState';
|
import { worldStateMixin } from '@/mixins/worldState';
|
||||||
|
|
||||||
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
|
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true';
|
||||||
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
|
const TIME_TRAVEL_ENABLED = import.meta.env.TIME_TRAVEL_ENABLED === 'true';
|
||||||
|
|
||||||
let sinon;
|
let sinon;
|
||||||
if (TIME_TRAVEL_ENABLED) {
|
if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
|
||||||
// eslint-disable-next-line global-require
|
(async () => {
|
||||||
sinon = await import('sinon');
|
sinon = await import('sinon');
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -878,7 +873,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
melior,
|
melior,
|
||||||
twitter,
|
bluesky,
|
||||||
facebook,
|
facebook,
|
||||||
instagram,
|
instagram,
|
||||||
tumblr,
|
tumblr,
|
||||||
@@ -951,24 +946,28 @@ export default {
|
|||||||
},
|
},
|
||||||
async jumpTime (amount) {
|
async jumpTime (amount) {
|
||||||
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
|
||||||
if (amount > 0) {
|
setTimeout(() => {
|
||||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
if (amount > 0) {
|
||||||
} else {
|
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||||
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
} else {
|
||||||
}
|
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
|
||||||
this.lastTimeJump = response.data.data.time;
|
}
|
||||||
this.triggerGetWorldState(true);
|
this.lastTimeJump = response.data.data.time;
|
||||||
|
this.triggerGetWorldState(true);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
async resetTime () {
|
async resetTime () {
|
||||||
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
|
||||||
const time = new Date(response.data.data.time);
|
const time = new Date(response.data.data.time);
|
||||||
Vue.config.clock.restore();
|
setTimeout(() => {
|
||||||
Vue.config.clock = sinon.useFakeTimers({
|
Vue.config.clock.restore();
|
||||||
now: time,
|
Vue.config.clock = sinon.useFakeTimers({
|
||||||
shouldAdvanceTime: true,
|
now: time,
|
||||||
});
|
shouldAdvanceTime: true,
|
||||||
this.lastTimeJump = response.data.data.time;
|
});
|
||||||
this.triggerGetWorldState(true);
|
this.lastTimeJump = response.data.data.time;
|
||||||
|
this.triggerGetWorldState(true);
|
||||||
|
}, 1000);
|
||||||
},
|
},
|
||||||
addExp () {
|
addExp () {
|
||||||
// @TODO: Name these variables better
|
// @TODO: Name these variables better
|
||||||
@@ -996,7 +995,6 @@ export default {
|
|||||||
async bossRage () {
|
async bossRage () {
|
||||||
await axios.post('/api/v4/debug/boss-rage');
|
await axios.post('/api/v4/debug/boss-rage');
|
||||||
},
|
},
|
||||||
|
|
||||||
async makeAdmin () {
|
async makeAdmin () {
|
||||||
await axios.post('/api/v4/debug/make-admin');
|
await axios.post('/api/v4/debug/make-admin');
|
||||||
// @TODO: Notification.text('You are now an admin!
|
// @TODO: Notification.text('You are now an admin!
|
||||||
@@ -1006,6 +1004,9 @@ export default {
|
|||||||
donate () {
|
donate () {
|
||||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||||
},
|
},
|
||||||
|
showBailey () {
|
||||||
|
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -168,7 +168,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -227,8 +227,8 @@ import debounce from 'lodash/debounce';
|
|||||||
import isEmail from 'validator/es/lib/isEmail';
|
import isEmail from 'validator/es/lib/isEmail';
|
||||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||||
import googleIcon from '@/assets/svg/google.svg';
|
import googleIcon from '@/assets/svg/google.svg?raw';
|
||||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
import appleIcon from '@/assets/svg/apple_black.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AuthForm',
|
name: 'AuthForm',
|
||||||
@@ -290,7 +290,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
hello.init({
|
hello.init({
|
||||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -220,7 +220,6 @@
|
|||||||
v-if="forgotPassword"
|
v-if="forgotPassword"
|
||||||
id="forgot-form"
|
id="forgot-form"
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
@keyup.enter="handleSubmit"
|
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -268,12 +267,11 @@
|
|||||||
v-if="resetPasswordSetNewOne"
|
v-if="resetPasswordSetNewOne"
|
||||||
id="reset-password-set-new-one-form"
|
id="reset-password-set-new-one-form"
|
||||||
@submit.prevent="handleSubmit"
|
@submit.prevent="handleSubmit"
|
||||||
@keyup.enter="handleSubmit"
|
|
||||||
>
|
>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="svg-icon habitica-logo color"
|
class="svg-icon habitica-logo"
|
||||||
v-html="icons.habiticaIcon"
|
v-html="icons.habiticaIcon"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -355,7 +353,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
@media only screen and (min-height: 1080px) {
|
@media only screen and (min-height: 1080px) {
|
||||||
.bottom-wrap-register {
|
.bottom-wrap-register {
|
||||||
@@ -491,7 +489,7 @@
|
|||||||
|
|
||||||
#top-background {
|
#top-background {
|
||||||
.seamless_stars_varied_opacity_repeat {
|
.seamless_stars_varied_opacity_repeat {
|
||||||
background-image: url('~@/assets/images/auth/seamless_stars_varied_opacity.png');
|
background-image: url('@/assets/images/auth/seamless_stars_varied_opacity.png');
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
@@ -510,7 +508,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.seamless_mountains_demo_repeat {
|
.seamless_mountains_demo_repeat {
|
||||||
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
|
background-image: url('@/assets/images/auth/seamless_mountains_demo.png');
|
||||||
background-repeat: repeat-x;
|
background-repeat: repeat-x;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
@@ -520,7 +518,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.midground_foreground_extended2 {
|
.midground_foreground_extended2 {
|
||||||
background-image: url('~@/assets/images/auth/midground_foreground_extended2.png');
|
background-image: url('@/assets/images/auth/midground_foreground_extended2.png');
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 1500px;
|
width: 1500px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -611,11 +609,11 @@ import isEmail from 'validator/es/lib/isEmail';
|
|||||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||||
import exclamation from '@/assets/svg/exclamation.svg';
|
import exclamation from '@/assets/svg/exclamation.svg?raw';
|
||||||
import gryphon from '@/assets/svg/gryphon.svg';
|
import gryphon from '@/assets/svg/gryphon.svg?raw';
|
||||||
import habiticaIcon from '@/assets/svg/logo-horizontal.svg';
|
import habiticaIcon from '@/assets/svg/logo-horizontal.svg?raw';
|
||||||
import googleIcon from '@/assets/svg/google.svg';
|
import googleIcon from '@/assets/svg/google.svg?raw';
|
||||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
import appleIcon from '@/assets/svg/apple_black.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [sanitizeRedirect],
|
mixins: [sanitizeRedirect],
|
||||||
@@ -726,9 +724,13 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
|
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
|
||||||
|
if (this.forgotPassword) {
|
||||||
|
if (this.$route.query.email) {
|
||||||
|
this.username = this.$route.query.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
hello.init({
|
hello.init({
|
||||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,105 +1,103 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="avatar-wrapper">
|
<div
|
||||||
|
v-if="member.preferences"
|
||||||
|
class="avatar"
|
||||||
|
:style="{width, height, paddingTop}"
|
||||||
|
:class="topLevelClassList"
|
||||||
|
@click.prevent="castEnd()"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="member.preferences"
|
class="character-sprites"
|
||||||
class="avatar"
|
:style="{margin: spritesMargin}"
|
||||||
:style="{width, height, paddingTop}"
|
|
||||||
:class="topLevelClassList"
|
|
||||||
@click.prevent="castEnd()"
|
|
||||||
>
|
>
|
||||||
<div
|
<template v-if="!avatarOnly">
|
||||||
class="character-sprites"
|
<!-- Mount Body-->
|
||||||
:style="{margin: spritesMargin}"
|
|
||||||
>
|
|
||||||
<template v-if="!avatarOnly">
|
|
||||||
<!-- Mount Body-->
|
|
||||||
<span
|
|
||||||
v-if="member.items.currentMount"
|
|
||||||
:class="'Mount_Body_' + member.items.currentMount"
|
|
||||||
></span>
|
|
||||||
</template>
|
|
||||||
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
|
|
||||||
<template v-for="(klass, item) in visualBuffs">
|
|
||||||
<span
|
|
||||||
v-if="member.stats.buffs[item] && showVisualBuffs"
|
|
||||||
:key="item"
|
|
||||||
:class="klass"
|
|
||||||
></span>
|
|
||||||
</template>
|
|
||||||
<!-- Show flower ALL THE TIME!!!-->
|
|
||||||
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
|
|
||||||
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
|
|
||||||
<!-- Show avatar only if not currently affected by visual buff-->
|
|
||||||
<template v-if="showAvatar()">
|
|
||||||
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
|
|
||||||
<span :class="[getGearClass('back'), specialMountClass]"></span>
|
|
||||||
<span :class="[skinClass, specialMountClass]"></span>
|
|
||||||
<!-- eslint-disable max-len-->
|
|
||||||
<span
|
|
||||||
:class="[shirtClass, specialMountClass]"
|
|
||||||
></span>
|
|
||||||
<!-- eslint-enable max-len-->
|
|
||||||
<span :class="['head_0', specialMountClass]"></span>
|
|
||||||
<!-- eslint-disable max-len-->
|
|
||||||
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
|
|
||||||
<!-- eslint-enable max-len-->
|
|
||||||
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
|
|
||||||
<template
|
|
||||||
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:key="type"
|
|
||||||
:class="[hairClass(type), specialMountClass]"
|
|
||||||
></span>
|
|
||||||
</template>
|
|
||||||
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
|
||||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
|
||||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
|
||||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
|
||||||
]"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
v-if="!hideGear('shield')"
|
|
||||||
:class="[getGearClass('shield'), specialMountClass]"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
v-if="!hideGear('weapon')"
|
|
||||||
:class="[getGearClass('weapon'), specialMountClass]"
|
|
||||||
class="weapon"
|
|
||||||
></span>
|
|
||||||
</template>
|
|
||||||
<!-- Resting-->
|
|
||||||
<span
|
<span
|
||||||
v-if="member.preferences.sleep"
|
v-if="member.items.currentMount"
|
||||||
class="zzz"
|
:class="'Mount_Body_' + member.items.currentMount"
|
||||||
></span>
|
></span>
|
||||||
<template v-if="!avatarOnly">
|
</template>
|
||||||
<!-- Mount Head-->
|
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
|
||||||
|
<template v-for="(klass, item) in visualBuffs">
|
||||||
|
<span
|
||||||
|
v-if="member.stats.buffs[item] && showVisualBuffs"
|
||||||
|
:key="item"
|
||||||
|
:class="klass"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
<!-- Show flower ALL THE TIME!!!-->
|
||||||
|
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
|
||||||
|
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
|
||||||
|
<!-- Show avatar only if not currently affected by visual buff-->
|
||||||
|
<template v-if="showAvatar()">
|
||||||
|
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
|
||||||
|
<span :class="[getGearClass('back'), specialMountClass]"></span>
|
||||||
|
<span :class="[skinClass, specialMountClass]"></span>
|
||||||
|
<!-- eslint-disable max-len-->
|
||||||
|
<span
|
||||||
|
:class="[shirtClass, specialMountClass]"
|
||||||
|
></span>
|
||||||
|
<!-- eslint-enable max-len-->
|
||||||
|
<span :class="['head_0', specialMountClass]"></span>
|
||||||
|
<!-- eslint-disable max-len-->
|
||||||
|
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
|
||||||
|
<!-- eslint-enable max-len-->
|
||||||
|
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
|
||||||
|
<template
|
||||||
|
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
v-if="member.items.currentMount"
|
:key="type"
|
||||||
:class="'Mount_Head_' + member.items.currentMount"
|
:class="[hairClass(type), specialMountClass]"
|
||||||
></span>
|
|
||||||
<!-- Pet-->
|
|
||||||
<span
|
|
||||||
class="current-pet"
|
|
||||||
:class="petClass"
|
|
||||||
></span>
|
></span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
<span :class="[getGearClass('body'), specialMountClass]"></span>
|
||||||
<class-badge
|
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||||
v-if="hasClass && !hideClassBadge"
|
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||||
class="under-avatar"
|
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||||
:member-class="member.stats.class"
|
<span
|
||||||
/>
|
:class="[
|
||||||
|
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="!hideGear('shield')"
|
||||||
|
:class="[getGearClass('shield'), specialMountClass]"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="!hideGear('weapon')"
|
||||||
|
:class="[getGearClass('weapon'), specialMountClass]"
|
||||||
|
class="weapon"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
|
<!-- Resting-->
|
||||||
|
<span
|
||||||
|
v-if="member.preferences.sleep"
|
||||||
|
class="zzz"
|
||||||
|
></span>
|
||||||
|
<template v-if="!avatarOnly">
|
||||||
|
<!-- Mount Head-->
|
||||||
|
<span
|
||||||
|
v-if="member.items.currentMount"
|
||||||
|
:class="'Mount_Head_' + member.items.currentMount"
|
||||||
|
></span>
|
||||||
|
<!-- Pet-->
|
||||||
|
<span
|
||||||
|
class="current-pet"
|
||||||
|
:class="petClass"
|
||||||
|
></span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<class-badge
|
||||||
|
v-if="hasClass && !hideClassBadge"
|
||||||
|
class="under-avatar"
|
||||||
|
:member-class="member.stats.class"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 141px;
|
width: 141px;
|
||||||
@@ -139,11 +137,6 @@
|
|||||||
filter: invert(100%);
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.weapon {
|
|
||||||
// the only one that is relative so that it fits into the parent div
|
|
||||||
position: relative !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.debug {
|
.debug {
|
||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
|
|
||||||
@@ -162,7 +155,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import some from 'lodash/some';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
import foolPet from '../mixins/foolPet';
|
import foolPet from '../mixins/foolPet';
|
||||||
@@ -210,11 +202,11 @@ export default {
|
|||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '140px',
|
default: '141px',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: '147px',
|
||||||
},
|
},
|
||||||
centerAvatar: {
|
centerAvatar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -329,11 +321,10 @@ export default {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
petClass () {
|
petClass () {
|
||||||
if (some(
|
const foolEvent = this.currentEventList?.find(event => moment()
|
||||||
this.currentEventList,
|
.isBetween(event.start, event.end) && event.aprilFools);
|
||||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
|
if (foolEvent) {
|
||||||
)) {
|
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||||
return this.foolPet(this.member.items.currentPet);
|
|
||||||
}
|
}
|
||||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.bottom-banner {
|
.bottom-banner {
|
||||||
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
|
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import sparkles from '@/assets/svg/sparkles-left.svg';
|
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data () {
|
data () {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="option in items"
|
v-for="option in items"
|
||||||
:key="option.key"
|
|
||||||
:id="option.imageName"
|
:id="option.imageName"
|
||||||
|
:key="option.key"
|
||||||
class="outer-option-background"
|
class="outer-option-background"
|
||||||
:class="{
|
:class="{
|
||||||
premium: Boolean(option.gem),
|
premium: Boolean(option.gem),
|
||||||
@@ -28,23 +28,22 @@
|
|||||||
v-if="!option.none"
|
v-if="!option.none"
|
||||||
class="sprite"
|
class="sprite"
|
||||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||||
:imageName="option.imageName"
|
|
||||||
:image-name="option.imageName"
|
:image-name="option.imageName"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="redline-outer"
|
class="redline-outer"
|
||||||
>
|
>
|
||||||
<div class="redline"></div>
|
<div class="redline"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gem from '@/assets/svg/gem.svg';
|
import gem from '@/assets/svg/gem.svg?raw';
|
||||||
import gold from '@/assets/svg/gold.svg';
|
import gold from '@/assets/svg/gold.svg?raw';
|
||||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||||
import Sprite from '@/components/ui/sprite.vue';
|
import Sprite from '@/components/ui/sprite.vue';
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.customize-options {
|
.customize-options {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.sub-menu {
|
.sub-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -30,8 +30,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import markdownDirective from '@/directives/markdown';
|
import markdownDirective from '@/directives/markdown';
|
||||||
|
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||||
|
|
||||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
directives: {
|
directives: {
|
||||||
@@ -39,7 +40,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bannedMessage () {
|
bannedMessage () {
|
||||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||||
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $white;
|
color: $white;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: $white;
|
color: $white;
|
||||||
@@ -134,7 +134,7 @@ label {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import closeIcon from '@/components/shared/closeIcon';
|
import closeIcon from '@/components/shared/closeIcon';
|
||||||
import checkCircleIcon from '@/assets/svg/check_circle.svg';
|
import checkCircleIcon from '@/assets/svg/check_circle.svg?raw';
|
||||||
import { MODALS } from '@/libs/consts';
|
import { MODALS } from '@/libs/consts';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -75,16 +75,20 @@
|
|||||||
class="box member-count"
|
class="box member-count"
|
||||||
@click="showMemberModal()"
|
@click="showMemberModal()"
|
||||||
>
|
>
|
||||||
<div
|
<div class="box-content">
|
||||||
class="svg-icon member-icon"
|
<div class="icon-number-row">
|
||||||
v-html="icons.memberIcon"
|
<div
|
||||||
></div>
|
class="svg-icon member-icon"
|
||||||
{{ challenge.memberCount }}
|
v-html="icons.memberIcon"
|
||||||
<div
|
></div>
|
||||||
v-once
|
<span class="number">{{ challenge.memberCount }}</span>
|
||||||
class="details"
|
</div>
|
||||||
>
|
<div
|
||||||
{{ $t('participantsTitle') }}
|
v-once
|
||||||
|
class="details"
|
||||||
|
>
|
||||||
|
{{ $t('participantsTitle') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
<div class="box">
|
||||||
@@ -259,7 +263,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss' scoped>
|
<style lang='scss' scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: $purple-200;
|
color: $purple-200;
|
||||||
@@ -304,7 +308,7 @@
|
|||||||
|
|
||||||
.box {
|
.box {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 1em;
|
padding: 0.5em;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||||
@@ -314,22 +318,69 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.member-count:hover {
|
&.member-count:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.box-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-number-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0.1em;
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 0.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: .2em;
|
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 0.4em;
|
|
||||||
color: $gray-200;
|
color: $gray-200;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1.15;
|
||||||
|
word-break: break-word;
|
||||||
|
max-height: 2.3em;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.member-count {
|
||||||
|
.icon-number-row {
|
||||||
|
.svg-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.1;
|
||||||
|
max-height: 2.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,9 +431,9 @@ import sidebarSection from '../sidebarSection';
|
|||||||
import userLink from '../userLink';
|
import userLink from '../userLink';
|
||||||
import groupLink from '../groupLink';
|
import groupLink from '../groupLink';
|
||||||
|
|
||||||
import gemIcon from '@/assets/svg/gem.svg';
|
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||||
import memberIcon from '@/assets/svg/member-icon.svg';
|
import memberIcon from '@/assets/svg/member-icon.svg?raw';
|
||||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||||
|
|
||||||
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
// Have to use this, because v-markdown creates p element in h3. Scoping doesn't work with it.
|
// Have to use this, because v-markdown creates p element in h3. Scoping doesn't work with it.
|
||||||
.challenge-title > p {
|
.challenge-title > p {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
.challenge {
|
.challenge {
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
@@ -377,14 +377,14 @@ import categoryTags from '../categories/categoryTags';
|
|||||||
import markdownDirective from '@/directives/markdown';
|
import markdownDirective from '@/directives/markdown';
|
||||||
import { mapState } from '@/libs/store';
|
import { mapState } from '@/libs/store';
|
||||||
|
|
||||||
import gemIcon from '@/assets/svg/gem.svg';
|
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||||
import memberIcon from '@/assets/svg/member-icon.svg';
|
import memberIcon from '@/assets/svg/member-icon.svg?raw';
|
||||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||||
import habitIcon from '@/assets/svg/habit.svg';
|
import habitIcon from '@/assets/svg/habit.svg?raw';
|
||||||
import todoIcon from '@/assets/svg/todo.svg';
|
import todoIcon from '@/assets/svg/todo.svg?raw';
|
||||||
import dailyIcon from '@/assets/svg/daily.svg';
|
import dailyIcon from '@/assets/svg/daily.svg?raw';
|
||||||
import rewardIcon from '@/assets/svg/reward.svg';
|
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
||||||
import officialIcon from '@/assets/svg/official.svg';
|
import officialIcon from '@/assets/svg/official.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -207,7 +207,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang='scss'>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#challenge-modal {
|
#challenge-modal {
|
||||||
h5 {
|
h5 {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
id="close-challenge-modal"
|
id="close-challenge-modal"
|
||||||
:title="$t('endChallenge')"
|
:title="$t('endChallenge')"
|
||||||
size="md"
|
size="md"
|
||||||
|
:hide-header="false"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
slot="modal-header"
|
slot="modal-header"
|
||||||
@@ -15,6 +16,15 @@
|
|||||||
>
|
>
|
||||||
{{ $t('endChallenge') }}
|
{{ $t('endChallenge') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
<button
|
||||||
|
class="close-button"
|
||||||
|
@click="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="svg-icon"
|
||||||
|
v-html="icons.close"
|
||||||
|
></div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<span
|
<span
|
||||||
@@ -28,28 +38,67 @@
|
|||||||
class="col-12"
|
class="col-12"
|
||||||
>
|
>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="support-habitica">
|
<div class="badge-section">
|
||||||
<!-- @TODO: Add challenge achievement badge here-->
|
<div
|
||||||
|
class="gems-left"
|
||||||
|
v-html="icons.gemsOrange"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="challenge-badge"
|
||||||
|
v-html="icons.endChallengeBadge"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="gems-right"
|
||||||
|
v-html="icons.gemsPurple"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<member-search-dropdown
|
<div class="search-input-wrapper">
|
||||||
:text="winnerText"
|
<div
|
||||||
:members="members"
|
class="search-icon"
|
||||||
:challenge-id="challengeId"
|
v-html="icons.search"
|
||||||
@member-selected="selectMember"
|
></div>
|
||||||
/>
|
<input
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="search-input"
|
||||||
|
type="text"
|
||||||
|
:placeholder="'@' + $t('username')"
|
||||||
|
@input="searchMembers"
|
||||||
|
@focus="showResults = true"
|
||||||
|
@blur="handleBlur"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="showResults && filteredMembers.length > 0"
|
||||||
|
class="search-results"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="member in filteredMembers"
|
||||||
|
:key="member._id"
|
||||||
|
class="search-result-item"
|
||||||
|
@mousedown="selectMember(member)"
|
||||||
|
>
|
||||||
|
{{ getMemberDisplayName(member) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button
|
<button
|
||||||
v-once
|
class="btn award-winner-btn"
|
||||||
class="btn btn-primary"
|
:class="{'has-winner': winner._id}"
|
||||||
|
:disabled="!winner._id"
|
||||||
@click="closeChallenge"
|
@click="closeChallenge"
|
||||||
>
|
>
|
||||||
{{ $t('awardWinners') }}
|
<span>{{ $t('awardWinners') }}</span>
|
||||||
|
<div
|
||||||
|
class="gem-icon"
|
||||||
|
v-html="icons.gem"
|
||||||
|
></div>
|
||||||
|
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -60,14 +109,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
<strong
|
||||||
|
v-once
|
||||||
|
class="delete-challenge-text"
|
||||||
|
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 refund-text">
|
||||||
|
{{ $t('deleteChallengeRefundDescription') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<button
|
<button
|
||||||
v-once
|
v-once
|
||||||
class="btn btn-danger"
|
class="btn btn-danger delete-challenge-btn"
|
||||||
@click="deleteChallenge()"
|
@click="deleteChallenge()"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
class="delete-icon"
|
||||||
|
v-html="icons.deleteIcon"
|
||||||
|
></div>
|
||||||
{{ $t('deleteChallenge') }}
|
{{ $t('deleteChallenge') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +140,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang='scss'>
|
||||||
@import '~@/assets/scss/colors.scss';
|
@import '@/assets/scss/colors.scss';
|
||||||
|
|
||||||
#close-challenge-modal {
|
#close-challenge-modal {
|
||||||
h2 {
|
h2 {
|
||||||
@@ -95,13 +154,185 @@
|
|||||||
.header-wrap {
|
.header-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.support-habitica {
|
.close-button {
|
||||||
background-image: url('~@/assets/svg/for-css/support-habitica-gems.svg');
|
position: absolute;
|
||||||
width: 325px;
|
top: 1rem;
|
||||||
height: 89px;
|
right: 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: $gray-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 384px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: $gray-200;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
padding-left: 36px;
|
||||||
|
padding-right: 12px;
|
||||||
|
border: 2px solid $gray-400;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: $purple-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $gray-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: $white;
|
||||||
|
border: 1px solid $gray-400;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.search-result-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-challenge-text {
|
||||||
|
color: $maroon-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-text {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: $gray-50;
|
||||||
|
margin-top: 0.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-challenge-btn {
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.award-winner-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: $white;
|
||||||
|
color: $gray-200;
|
||||||
|
border: 1px solid $gray-400;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-winner {
|
||||||
|
background-color: $purple-200;
|
||||||
|
color: $white;
|
||||||
|
border-color: $purple-200;
|
||||||
|
|
||||||
|
.gem-icon {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(.has-winner):not(:disabled) {
|
||||||
|
background-color: $gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gem-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: $gems-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 0;
|
||||||
|
|
||||||
|
.gems-left, .gems-right {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.challenge-badge {
|
||||||
|
width: 48px;
|
||||||
|
height: 52px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer, .modal-header {
|
.modal-footer, .modal-header {
|
||||||
@@ -123,21 +354,37 @@
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
color: $gray-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
|
import searchIcon from '@/assets/svg/for-css/search.svg?raw';
|
||||||
|
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||||
|
import closeIcon from '@/assets/svg/close.svg?raw';
|
||||||
|
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||||
|
import endChallengeBadge from '@/assets/svg/for-css/end_challenge_badge.svg?raw';
|
||||||
|
import gemsOrange from '@/assets/svg/for-css/orange100_red100_yellow100_gems.svg?raw';
|
||||||
|
import gemsPurple from '@/assets/svg/for-css/purple200_green10_blue100_gems.svg?raw';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
memberSearchDropdown,
|
|
||||||
},
|
|
||||||
props: ['challengeId', 'members', 'prize', 'flagCount'],
|
props: ['challengeId', 'members', 'prize', 'flagCount'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
winner: {},
|
winner: {},
|
||||||
|
searchTerm: '',
|
||||||
|
showResults: false,
|
||||||
|
filteredMembers: [],
|
||||||
|
icons: Object.freeze({
|
||||||
|
search: searchIcon,
|
||||||
|
deleteIcon,
|
||||||
|
close: closeIcon,
|
||||||
|
gem: gemIcon,
|
||||||
|
endChallengeBadge,
|
||||||
|
gemsOrange,
|
||||||
|
gemsPurple,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -150,8 +397,35 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
searchMembers () {
|
||||||
|
if (!this.searchTerm) {
|
||||||
|
this.filteredMembers = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchLower = this.searchTerm.toLowerCase().replace('@', '');
|
||||||
|
this.filteredMembers = this.members.filter(member => {
|
||||||
|
const username = member.auth?.local?.username || '';
|
||||||
|
const displayName = member.profile?.name || '';
|
||||||
|
return username.toLowerCase().includes(searchLower)
|
||||||
|
|| displayName.toLowerCase().includes(searchLower);
|
||||||
|
}).slice(0, 10);
|
||||||
|
},
|
||||||
|
getMemberDisplayName (member) {
|
||||||
|
if (member.auth?.local?.username) {
|
||||||
|
return `@${member.auth.local.username}`;
|
||||||
|
}
|
||||||
|
return member.profile?.name || '';
|
||||||
|
},
|
||||||
selectMember (member) {
|
selectMember (member) {
|
||||||
this.winner = member;
|
this.winner = member;
|
||||||
|
this.searchTerm = this.getMemberDisplayName(member);
|
||||||
|
this.showResults = false;
|
||||||
|
},
|
||||||
|
handleBlur () {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showResults = false;
|
||||||
|
}, 200);
|
||||||
},
|
},
|
||||||
async closeChallenge () {
|
async closeChallenge () {
|
||||||
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user