mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 06:37:23 +01:00
Compare commits
130 Commits
phillip/co
...
fiz/case-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1ab9cb6ca | ||
|
|
ffa73698a5 | ||
|
|
2feadd6125 | ||
|
|
efe0b3cd9e | ||
|
|
96731da380 | ||
|
|
0c5dd5d8b5 | ||
|
|
2f943a22e6 | ||
|
|
666184d7e4 | ||
|
|
17d22dda3f | ||
|
|
d1a18c121d | ||
|
|
836d7f3991 | ||
|
|
ace9c3c46a | ||
|
|
068640311e | ||
|
|
f26d2a59ae | ||
|
|
03c7e9172e | ||
|
|
6fdc072ec3 | ||
|
|
e68661c04b | ||
|
|
4f567592ea | ||
|
|
63c9b7a894 | ||
|
|
eaec39188e | ||
|
|
ba6940eb81 | ||
|
|
f8a3e4d673 | ||
|
|
2727da6f6c | ||
|
|
fa97852e38 | ||
|
|
2c7da25a25 | ||
|
|
9a072e3e76 | ||
|
|
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
.gitignore
vendored
2
.gitignore
vendored
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
|||||||
|
|
||||||
# mongodb replica set for local dev
|
# mongodb replica set for local dev
|
||||||
mongodb-*.tgz
|
mongodb-*.tgz
|
||||||
/mongodb-data
|
/mongodb-data*
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|||||||
@@ -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
16
package.json
16
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.0",
|
||||||
"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,22 +98,22 @@
|
|||||||
"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",
|
||||||
"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",
|
||||||
"apidoc": "gulp apidoc",
|
"apidoc": "gulp apidoc",
|
||||||
"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();
|
||||||
|
|
||||||
|
const result = await Promise.allSettled([
|
||||||
|
cronWrapper(req, res),
|
||||||
|
cronWrapper(req, res),
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
await recoverCron(status, locals);
|
const runResult = await cronWrapper(req, res);
|
||||||
throw new Error('no exception when recoverCron runs 5 times');
|
if (runResult !== null) {
|
||||||
} catch (err) {
|
reject(new Error('cron ran more than once'));
|
||||||
expect(status.times).to.eql(5);
|
} else {
|
||||||
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
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.
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
@@ -60,12 +61,12 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||||
|
|
||||||
// test response values
|
// test response values
|
||||||
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||||
expect(heroRes.contributor.level).to.equal(1);
|
expect(heroRes.contributor.level).to.equal(1);
|
||||||
expect(heroRes.purchased.ads).to.equal(true);
|
expect(heroRes.purchased.ads).to.equal(true);
|
||||||
// test hero values
|
// test hero values
|
||||||
await hero.sync();
|
await hero.sync();
|
||||||
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
|
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
|
||||||
expect(hero.contributor.level).to.equal(1);
|
expect(hero.contributor.level).to.equal(1);
|
||||||
expect(hero.purchased.ads).to.equal(true);
|
expect(hero.purchased.ads).to.equal(true);
|
||||||
expect(hero.auth.blocked).to.equal(prevBlockState);
|
expect(hero.auth.blocked).to.equal(prevBlockState);
|
||||||
@@ -136,12 +137,12 @@ describe('PUT /heroes/:heroId', () => {
|
|||||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||||
|
|
||||||
// test response values
|
// test response values
|
||||||
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
|
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||||
expect(heroRes.contributor.level).to.equal(6);
|
expect(heroRes.contributor.level).to.equal(6);
|
||||||
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
|
||||||
// test hero values
|
// test hero values
|
||||||
await hero.sync();
|
await hero.sync();
|
||||||
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
|
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
|
||||||
expect(hero.contributor.level).to.equal(6);
|
expect(hero.contributor.level).to.equal(6);
|
||||||
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
|
||||||
});
|
});
|
||||||
|
|||||||
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
56
test/api/v3/integration/members/GET-members_username.test.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
import common from '../../../../../website/common';
|
||||||
|
|
||||||
|
describe('GET /members/username/:username', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates req.params.username', async () => {
|
||||||
|
await expect(user.get('/members/username/')).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a member\'s public data only', async () => {
|
||||||
|
// make sure user has all the fields that can be returned by the getMember call
|
||||||
|
const member = await generateUser({
|
||||||
|
contributor: { level: 1 },
|
||||||
|
backer: { tier: 3 },
|
||||||
|
preferences: {
|
||||||
|
costume: false,
|
||||||
|
background: 'volcano',
|
||||||
|
},
|
||||||
|
secret: {
|
||||||
|
text: 'Clark Kent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const memberRes = await user.get(`/members/username/${member.auth.local.username}`);
|
||||||
|
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||||
|
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||||
|
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||||
|
]);
|
||||||
|
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||||
|
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||||
|
'size', 'hair', 'skin', 'shirt',
|
||||||
|
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||||
|
].sort());
|
||||||
|
|
||||||
|
expect(memberRes.stats.maxMP).to.exist;
|
||||||
|
expect(memberRes.stats.maxHealth).to.equal(common.maxHealth);
|
||||||
|
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||||
|
expect(memberRes.inbox.optOut).to.exist;
|
||||||
|
expect(memberRes.inbox.canReceive).to.exist;
|
||||||
|
expect(memberRes.inbox.messages).to.not.exist;
|
||||||
|
expect(memberRes.secret).to.not.exist;
|
||||||
|
|
||||||
|
expect(memberRes.blocks).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
104
test/api/v4/inbox/POST-inbox_message_like.test.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import find from 'lodash/find';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the messages array if the uniqueMessageId has the like flag
|
||||||
|
* @param {InboxMessage[]} messages
|
||||||
|
* @param {String} uniqueMessageId
|
||||||
|
* @param {String} userId
|
||||||
|
* @param {Boolean} likeStatus
|
||||||
|
*/
|
||||||
|
function expectMessagesLikeStatus (messages, uniqueMessageId, userId, likeStatus) {
|
||||||
|
const messageToCheck = find(messages, { uniqueMessageId });
|
||||||
|
|
||||||
|
expect(messageToCheck.likes[userId]).to.equal(likeStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line mocha/no-exclusive-tests
|
||||||
|
describe('POST /inbox/like-private-message/:messageId', () => {
|
||||||
|
let userToSendMessage;
|
||||||
|
const getLikeUrl = messageId => `/inbox/like-private-message/${messageId}`;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
userToSendMessage = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when private message is not found', async () => {
|
||||||
|
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('messageGroupChatNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('likes a message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||||
|
|
||||||
|
const senderMessages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(senderMessages, uniqueMessageId, receiver._id, true);
|
||||||
|
|
||||||
|
const receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows a user to like their own private message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await userToSendMessage.post(getLikeUrl(uniqueMessageId));
|
||||||
|
expect(likeResult.likes[userToSendMessage._id]).to.equal(true);
|
||||||
|
|
||||||
|
const messages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
expectMessagesLikeStatus(messages, uniqueMessageId, userToSendMessage._id, true);
|
||||||
|
|
||||||
|
const receiversMessages = await receiver.get('/inbox/messages');
|
||||||
|
|
||||||
|
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlikes a message', async () => {
|
||||||
|
const receiver = await generateUser();
|
||||||
|
|
||||||
|
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
|
||||||
|
message: 'some message :)',
|
||||||
|
toUserId: receiver._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { uniqueMessageId } = sentMessageResult.message;
|
||||||
|
|
||||||
|
const likeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
|
||||||
|
expect(likeResult.likes[receiver._id]).to.equal(true);
|
||||||
|
|
||||||
|
const unlikeResult = await receiver.post(getLikeUrl(uniqueMessageId));
|
||||||
|
|
||||||
|
expect(unlikeResult.likes[receiver._id]).to.equal(false);
|
||||||
|
|
||||||
|
const messages = await userToSendMessage.get('/inbox/messages');
|
||||||
|
|
||||||
|
const messageToCheck = find(messages, { id: sentMessageResult.message.id });
|
||||||
|
expect(messageToCheck.likes[receiver._id]).to.equal(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,11 +3,12 @@ module.exports = {
|
|||||||
root: true,
|
root: true,
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
|
es2021: true,
|
||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
'habitrpg/lib/vue',
|
'habitrpg/lib/vue',
|
||||||
],
|
],
|
||||||
ignorePatterns: ['dist/', 'node_modules/'],
|
ignorePatterns: ['dist/', 'node_modules/', '*.d.ts'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
@@ -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>
|
||||||
11815
website/client/package-lock.json
generated
11815
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);
|
||||||
|
|
||||||
|
// Check if we need to show password change success message
|
||||||
|
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) {
|
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||||
this.hideLoadingScreen();
|
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;
|
||||||
|
|||||||
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';
|
||||||
@@ -101,8 +101,7 @@
|
|||||||
|
|
||||||
.btn-secondary,
|
.btn-secondary,
|
||||||
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
|
.dropdown > .btn-secondary.dropdown-toggle:not(.btn-success),
|
||||||
.show > .btn-secondary.dropdown-toggle:not(.btn-success)
|
.show > .btn-secondary.dropdown-toggle:not(.btn-success) {
|
||||||
{
|
|
||||||
background: $white;
|
background: $white;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
color: $gray-50;
|
color: $gray-50;
|
||||||
@@ -298,6 +297,16 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-flat,
|
||||||
|
.dropdown > .btn-flat.dropdown-toggle:not(.btn-success),
|
||||||
|
.show > .btn-flat.dropdown-toggle:not(.btn-success) {
|
||||||
|
&.with-icon {
|
||||||
|
.svg-icon.color {
|
||||||
|
color: var(--icon-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-cancel {
|
.btn-cancel {
|
||||||
color: $blue-10;
|
color: $blue-10;
|
||||||
}
|
}
|
||||||
@@ -307,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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,7 +38,12 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-min-width {
|
||||||
|
.dropdown-menu {
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shared dropdown-item styles
|
// shared dropdown-item styles
|
||||||
@@ -54,6 +59,8 @@
|
|||||||
color: $gray-50 !important;
|
color: $gray-50 !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
--dropdown-item-hover-icon-color: #{$gray-200};
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
@@ -88,7 +95,7 @@
|
|||||||
|
|
||||||
&:not(:hover) {
|
&:not(:hover) {
|
||||||
.with-icon .svg-icon {
|
.with-icon .svg-icon {
|
||||||
color: $gray-200;
|
color: var(dropdown-item-hover-icon-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +158,7 @@
|
|||||||
|
|
||||||
// selectList.vue items sizing
|
// selectList.vue items sizing
|
||||||
.selectListItem .dropdown-item {
|
.selectListItem .dropdown-item {
|
||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 1rem 0.25rem 0.75rem;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
&:active, &:hover, &:focus, &.active {
|
&:active, &:hover, &:focus, &.active {
|
||||||
|
|||||||
@@ -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 |
@@ -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 () {
|
||||||
|
|||||||
@@ -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({
|
const { textKey } = achievementItem;
|
||||||
key: nestedKey,
|
if (textKey !== undefined) {
|
||||||
text,
|
return i18n.t(textKey, { count });
|
||||||
achievementType: key,
|
|
||||||
modified: false,
|
|
||||||
path: `${basePath}.${key}.${nestedKey}`,
|
|
||||||
value: value[nestedKey],
|
|
||||||
valueIsInteger,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
const { singularTextKey } = achievementItem;
|
||||||
const valueIsInteger = self.integerTypes.includes(key);
|
if (singularTextKey !== undefined) {
|
||||||
achievements.push({
|
return i18n.t(singularTextKey, { count });
|
||||||
key,
|
|
||||||
text: getText(allAchievements[key]),
|
|
||||||
modified: false,
|
|
||||||
path: `${basePath}.${key}`,
|
|
||||||
value: ownedAchievements[key],
|
|
||||||
valueIsInteger,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
return '';
|
||||||
|
|
||||||
for (const key of Object.keys(allAchievements)) {
|
|
||||||
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.achievements = achievements;
|
|
||||||
self.nestedAchievements = nestedAchievements;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
|
<label class="col-sm-3 col-form-label">
|
||||||
Payment method:
|
Payment method:
|
||||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
</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
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
class="form-group row"
|
||||||
|
>
|
||||||
|
<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 v-if="hero.purchased.plan.planId">
|
|
||||||
Payment schedule ("basic-earned" is monthly):
|
|
||||||
<strong>{{ hero.purchased.plan.planId }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
|
|
||||||
Group plan ID:
|
|
||||||
<strong>{{ hero.purchased.plan.owner }}</strong>
|
|
||||||
</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,6 +946,7 @@ 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 });
|
||||||
|
setTimeout(() => {
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
|
||||||
} else {
|
} else {
|
||||||
@@ -958,10 +954,12 @@ export default {
|
|||||||
}
|
}
|
||||||
this.lastTimeJump = response.data.data.time;
|
this.lastTimeJump = response.data.data.time;
|
||||||
this.triggerGetWorldState(true);
|
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);
|
||||||
|
setTimeout(() => {
|
||||||
Vue.config.clock.restore();
|
Vue.config.clock.restore();
|
||||||
Vue.config.clock = sinon.useFakeTimers({
|
Vue.config.clock = sinon.useFakeTimers({
|
||||||
now: time,
|
now: time,
|
||||||
@@ -969,6 +967,7 @@ export default {
|
|||||||
});
|
});
|
||||||
this.lastTimeJump = response.data.data.time;
|
this.lastTimeJump = response.data.data.time;
|
||||||
this.triggerGetWorldState(true);
|
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: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="member.preferences"
|
v-if="member.preferences"
|
||||||
class="avatar"
|
class="avatar"
|
||||||
:style="{width, height, paddingTop}"
|
:style="{width, height, paddingTop}"
|
||||||
:class="backgroundClass"
|
:class="topLevelClassList"
|
||||||
@click.prevent="castEnd()"
|
@click.prevent="castEnd()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -55,7 +55,11 @@
|
|||||||
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
|
||||||
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
<span :class="[getGearClass('head'), specialMountClass]"></span>
|
||||||
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
|
||||||
<span :class="['hair_flower_' + member.preferences.hair.flower, specialMountClass]"></span>
|
<span
|
||||||
|
:class="[
|
||||||
|
'hair_flower_' + member.preferences.hair.flower, specialMountClass
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
<span
|
<span
|
||||||
v-if="!hideGear('shield')"
|
v-if="!hideGear('shield')"
|
||||||
:class="[getGearClass('shield'), specialMountClass]"
|
:class="[getGearClass('shield'), specialMountClass]"
|
||||||
@@ -63,6 +67,7 @@
|
|||||||
<span
|
<span
|
||||||
v-if="!hideGear('weapon')"
|
v-if="!hideGear('weapon')"
|
||||||
:class="[getGearClass('weapon'), specialMountClass]"
|
:class="[getGearClass('weapon'), specialMountClass]"
|
||||||
|
class="weapon"
|
||||||
></span>
|
></span>
|
||||||
</template>
|
</template>
|
||||||
<!-- Resting-->
|
<!-- Resting-->
|
||||||
@@ -92,19 +97,27 @@
|
|||||||
</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;
|
||||||
height: 147px;
|
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.centered-avatar {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetting the additional padding
|
||||||
|
margin-bottom: -0.5rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-sprites {
|
.character-sprites {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-sprites span {
|
.character-sprites span {
|
||||||
@@ -123,22 +136,49 @@
|
|||||||
.invert {
|
.invert {
|
||||||
filter: invert(100%);
|
filter: invert(100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
border: 1px solid red;
|
||||||
|
|
||||||
|
.character-sprites {
|
||||||
|
border: 1px solid blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weapon {
|
||||||
|
border: 1px solid green;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
border: 1px solid yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
</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';
|
||||||
|
|
||||||
import ClassBadge from '@/components/members/classBadge';
|
import ClassBadge from '@/components/members/classBadge';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO replace avatarOnly with multiple options like
|
||||||
|
* - showMount
|
||||||
|
* - showPet
|
||||||
|
* - showBackground
|
||||||
|
* - showWeapons
|
||||||
|
*/
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ClassBadge,
|
ClassBadge,
|
||||||
},
|
},
|
||||||
mixins: [foolPet],
|
mixins: [foolPet],
|
||||||
props: {
|
props: {
|
||||||
|
debugMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
member: {
|
member: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -156,14 +196,21 @@ export default {
|
|||||||
},
|
},
|
||||||
overrideAvatarGear: {
|
overrideAvatarGear: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
default (data) {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: String,
|
||||||
default: 140,
|
default: '141px',
|
||||||
},
|
},
|
||||||
height: {
|
height: {
|
||||||
type: Number,
|
type: String,
|
||||||
default: 147,
|
default: '147px',
|
||||||
|
},
|
||||||
|
centerAvatar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
spritesMargin: {
|
spritesMargin: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -171,11 +218,16 @@ export default {
|
|||||||
},
|
},
|
||||||
overrideTopPadding: {
|
overrideTopPadding: {
|
||||||
type: String,
|
type: String,
|
||||||
|
default: null,
|
||||||
},
|
},
|
||||||
showVisualBuffs: {
|
showVisualBuffs: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
showWeapon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
@@ -204,6 +256,19 @@ export default {
|
|||||||
|
|
||||||
return val;
|
return val;
|
||||||
},
|
},
|
||||||
|
topLevelClassList () {
|
||||||
|
const classes = [this.backgroundClass];
|
||||||
|
|
||||||
|
if (this.debugMode) {
|
||||||
|
classes.push('debug');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.centerAvatar) {
|
||||||
|
classes.push('centered-avatar');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
},
|
||||||
backgroundClass () {
|
backgroundClass () {
|
||||||
if (this.member) {
|
if (this.member) {
|
||||||
const { background } = this.member.preferences;
|
const { background } = this.member.preferences;
|
||||||
@@ -256,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 '';
|
||||||
@@ -290,6 +354,10 @@ export default {
|
|||||||
},
|
},
|
||||||
hideGear (gearType) {
|
hideGear (gearType) {
|
||||||
if (!this.member) return true;
|
if (!this.member) return true;
|
||||||
|
if (!this.showWeapon) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (gearType === 'weapon') {
|
if (gearType === 'weapon') {
|
||||||
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +28,6 @@
|
|||||||
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
|
||||||
@@ -43,8 +42,8 @@
|
|||||||
</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 {
|
||||||
|
|||||||
@@ -259,7 +259,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;
|
||||||
@@ -380,9 +380,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 {
|
||||||
|
|||||||
@@ -81,7 +81,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 {
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.support-habitica {
|
.support-habitica {
|
||||||
background-image: url('~@/assets/svg/for-css/support-habitica-gems.svg');
|
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
|
||||||
width: 325px;
|
width: 325px;
|
||||||
height: 89px;
|
height: 89px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user