Compare commits
192 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c80cd83b18 | |||
| e31ce11052 | |||
| c4688b77fd | |||
| 9ab79ef225 | |||
| 7e4c8938e5 | |||
| 604b866113 | |||
| c0afae1e84 | |||
| 61dc94ada0 | |||
| f71ee7be18 | |||
| 8935885637 | |||
| 1ebda8281b | |||
| eae8ed946f | |||
| d7fe140012 | |||
| c317d1d040 | |||
| d63aafc7f1 | |||
| 08b2be684b | |||
| 4c653aa511 | |||
| 0896837528 | |||
| 8a1c1af461 | |||
| db7d1d0e14 | |||
| 6253359d31 | |||
| ce4bbe5552 | |||
| 3a4abac8b0 | |||
| 44eb245184 | |||
| 050fd15c97 | |||
| e1f1a264b4 | |||
| 3bfcc4a605 | |||
| 4beac98383 | |||
| 4da794271d | |||
| b18eb0d3f8 | |||
| 79f6f47b16 | |||
| 6ff8254137 | |||
| 1c00d7de5b | |||
| 61d970db86 | |||
| b911ecdcf0 | |||
| 74d0b25c54 | |||
| 830c4fb926 | |||
| 8aad136622 | |||
| 8d80792093 | |||
| 79f83d87d0 | |||
| c9bf157c23 | |||
| 3be42e9545 | |||
| 8a7b8f92d4 | |||
| e54425f947 | |||
| d953ea14da | |||
| 0c179fee2f | |||
| 05e229ccb0 | |||
| b29d937806 | |||
| 670b6a1563 | |||
| 44ded945dd | |||
| 08f1e2b273 | |||
| ca80f4ee33 | |||
| 65cbee9e75 | |||
| 0f8563c14e | |||
| c4117f99ed | |||
| 27aca19c8c | |||
| 80c18ffadd | |||
| bf2c4eb501 | |||
| ab1828c914 | |||
| b32f79f682 | |||
| 7b69289069 | |||
| a1fb80868f | |||
| 9d4fb80d15 | |||
| 43392f4952 | |||
| 6e46794822 | |||
| 43cad86201 | |||
| 734eae64a5 | |||
| 032c95d5c8 | |||
| b40411e219 | |||
| 86410f6bb7 | |||
| 8d2ada1463 | |||
| 95a3c932c5 | |||
| 51d1f6b86a | |||
| 809bacd10b | |||
| b0f29211a8 | |||
| 26f5bf554e | |||
| a1ec42c0b2 | |||
| 8674ea55f9 | |||
| c421da34cc | |||
| fb79b9e128 | |||
| 375b6719e9 | |||
| c9bed96077 | |||
| 1ccff7f1dd | |||
| 36461cbbdf | |||
| 427f73f664 | |||
| d796848887 | |||
| 80a2e31c8e | |||
| 85d290a1fa | |||
| 1bc756ee93 | |||
| ea44af0929 | |||
| 739963762e | |||
| b02ae35b28 | |||
| ace8e2f0bd | |||
| e90175b5d6 | |||
| 403fa18511 | |||
| 7dd6227845 | |||
| 4c7306491b | |||
| 1164f5e5f5 | |||
| c6937a0409 | |||
| 092d6726b8 | |||
| 2a67de698b | |||
| cfc8620865 | |||
| 714a18ce5c | |||
| 41e22640f5 | |||
| 8747a2d1b6 | |||
| 721ae0872f | |||
| 6658abbcd9 | |||
| 68099626eb | |||
| ff9780c5e5 | |||
| 56aaca5c0c | |||
| c22a2ef9a8 | |||
| a3ba1d19ee | |||
| 3c5a7b65c5 | |||
| 1ebe57b114 | |||
| c218b8d56c | |||
| ef99943646 | |||
| 29f6bf7dc6 | |||
| f7af8ec910 | |||
| f4ba6b2186 | |||
| a3b59d9254 | |||
| e813428472 | |||
| ebd22296df | |||
| db0009ebad | |||
| 613895e294 | |||
| be622e8b10 | |||
| a7c3c10ff4 | |||
| 1831c0dd79 | |||
| 8fe563aa37 | |||
| e5f1f3b279 | |||
| 446122d7b8 | |||
| 93335352ec | |||
| 6e24cf0fe1 | |||
| 378325a8a2 | |||
| f60b50d6fa | |||
| 785da1391f | |||
| adff828c91 | |||
| 5cdcbc5310 | |||
| 84a52002c8 | |||
| 671c90a593 | |||
| 261da70274 | |||
| c87803caca | |||
| c1705550c7 | |||
| 119102ff61 | |||
| 933fc4d882 | |||
| 31a83097be | |||
| 6cdc59d913 | |||
| ca4efa21cf | |||
| aeba14f2e9 | |||
| 1c94c1a968 | |||
| 26767f598b | |||
| 5d202c7617 | |||
| 643d3802cc | |||
| 5ee33f219a | |||
| 4e93874483 | |||
| 8bf44ce47a | |||
| 458bde1d13 | |||
| 6899731937 | |||
| 3101abbb08 | |||
| 537313a21e | |||
| 453383af9f | |||
| 58aa1ac2f3 | |||
| a979fc3843 | |||
| 9f91775e78 | |||
| 61ca931e66 | |||
| 2888f843e3 | |||
| b947c714f0 | |||
| 783b8995b8 | |||
| 8db2fb8015 | |||
| 1bcf2dfe80 | |||
| 805641b6cf | |||
| 5b0584fc5e | |||
| 99429d9d48 | |||
| 6ffc28f04e | |||
| 66ed0a350b | |||
| 6b2e9f16e2 | |||
| 7f6a2a0700 | |||
| 05008b20d3 | |||
| 6cae168adb | |||
| 72a9506b9f | |||
| af96cd0488 | |||
| 60204bef06 | |||
| 4c21d9e560 | |||
| fa38b22003 | |||
| d03e5e93b0 | |||
| 26437e7e2e | |||
| dc9800d88a | |||
| 39fcb3e876 | |||
| e47b0982c8 | |||
| 92f217775b | |||
| daf2c354d6 | |||
| 2ba6e972c3 | |||
| 6bacb89271 |
@@ -10,6 +10,7 @@ dist/
|
||||
dist-client/
|
||||
apidoc_build/
|
||||
content_cache/
|
||||
i18n_cache/
|
||||
node_modules/
|
||||
|
||||
# Old migrations, disabled
|
||||
|
||||
@@ -8,13 +8,8 @@
|
||||
|
||||
# Requesting a feature
|
||||
|
||||
Habitica uses [Trello](https://trello.com/b/EpoYEYod/habitica) to track feature requests. [Read more](https://trello.com/c/odmhIqyW/440-read-first-table-of-contents).
|
||||
Habitica uses [this Google form](https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link) to track feature requests. Please post there rather than creating an issue.
|
||||
|
||||
# Contributing Code
|
||||
|
||||
See [Contributing to Habitica](http://habitica.fandom.com/wiki/Contributing_to_Habitica#Coders_.28Web_.26_Mobile.29)
|
||||
|
||||
## Issue Triage [](https://www.codetriage.com/habitrpg/habitica)
|
||||
|
||||
You can triage issues which may include reproducing bug reports or asking for vital information, such as version numbers or reproduction instructions. If you would like to start triaging issues, one easy way to get started is to [subscribe to habitrpg on CodeTriage](https://www.codetriage.com/habitrpg/habitica).
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ website/transpiled-babel/
|
||||
website/common/transpiled-babel/
|
||||
node_modules
|
||||
content_cache
|
||||
i18n_cache
|
||||
apidoc_build
|
||||
*.swp
|
||||
.idea*
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
* Code is GPL v3 licensed:
|
||||
This Source Code is subject to the terms of the GNU General Public License, v. 3.0.
|
||||
If a copy of the GPL was not distributed with this file, You can obtain one at http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
If a copy of the GPL was not distributed with this file, you can obtain one at http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
* Assets and content designed for Mozilla BrowserQuest are licensed under CC-BY-SA 3.0:
|
||||
http://creativecommons.org/licenses/by-sa/3.0/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Habitica  [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [](https://www.codetriage.com/habitrpg/habitica)
|
||||
Habitica  [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
===============
|
||||
|
||||
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
|
||||
|
||||
@@ -11,10 +11,16 @@ gulp.task('build:babel:common', () => gulp.src('website/common/script/**/*.js')
|
||||
|
||||
gulp.task('build:babel', gulp.parallel('build:babel:server', 'build:babel:common', done => done()));
|
||||
|
||||
gulp.task('build:cache', gulp.parallel(
|
||||
'cache:content',
|
||||
'cache:i18n',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
gulp.task('build:prod', gulp.series(
|
||||
'build:babel',
|
||||
'apidoc',
|
||||
'content:cache',
|
||||
'build:cache',
|
||||
done => done(),
|
||||
));
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import gulp from 'gulp';
|
||||
import fs from 'fs';
|
||||
|
||||
// TODO parallelize, use gulp file helpers
|
||||
gulp.task('content:cache', done => {
|
||||
gulp.task('cache:content', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
@@ -32,3 +32,32 @@ gulp.task('content:cache', done => {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('cache:i18n', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { BROWSER_SCRIPT_CACHE_PATH, geti18nBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
fs.mkdirSync(BROWSER_SCRIPT_CACHE_PATH);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
|
||||
// create and save the i18n browser script for each language
|
||||
langCodes.forEach(languageCode => {
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}${languageCode}.js`,
|
||||
geti18nBrowserScript(languageCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
@@ -161,4 +161,4 @@ gulp.task('sprites:checkCompiledDimensions', gulp.series('sprites:main', 'sprite
|
||||
done();
|
||||
}));
|
||||
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions', done => done()));
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:checkCompiledDimensions', done => done()));
|
||||
|
||||
@@ -13,11 +13,11 @@ const gulp = require('gulp');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-cache'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-content'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-cache'); // 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-sprites'); // eslint-disable-line global-require
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.140.11",
|
||||
"version": "4.144.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/register": "^7.9.0",
|
||||
"@google-cloud/trace-agent": "^4.2.5",
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/register": "^7.10.1",
|
||||
"@google-cloud/trace-agent": "^5.0.0",
|
||||
"@slack/client": "^4.12.0",
|
||||
"accepts": "^1.3.5",
|
||||
"amazon-payments": "^0.2.8",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^2.2.0",
|
||||
"apple-auth": "^1.0.5",
|
||||
"apple-auth": "^1.0.6",
|
||||
"bcrypt": "^3.0.8",
|
||||
"body-parser": "^1.18.3",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^1.4.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.3.6",
|
||||
"csv-stringify": "^5.5.0",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"gulp-imagemin": "^6.2.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.9.0",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"habitica-markdown": "^2.0.0",
|
||||
"helmet": "^3.22.0",
|
||||
"image-size": "^0.8.3",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
@@ -46,9 +46,9 @@
|
||||
"lodash": "^4.17.15",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment": "^2.26.0",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.9.9",
|
||||
"mongoose": "^5.9.18",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^1.0.2",
|
||||
@@ -100,6 +100,7 @@
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"postinstall": "gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
@@ -111,7 +112,7 @@
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^7.2.0",
|
||||
"monk": "^7.3.0",
|
||||
"require-again": "^2.0.0",
|
||||
"sinon": "^7.2.4",
|
||||
"sinon-chai": "^3.5.0",
|
||||
|
||||
@@ -879,6 +879,11 @@ describe('cron', () => {
|
||||
tasksByType.todos.push(task);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
});
|
||||
|
||||
it('should make uncompleted todos redder', () => {
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
cron({
|
||||
@@ -887,6 +892,15 @@ describe('cron', () => {
|
||||
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||
});
|
||||
|
||||
it('should not make completed todos redder', () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
||||
});
|
||||
|
||||
it('should add history of completed todos to user history', () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
@@ -898,17 +912,13 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('should remove completed todos from users taskOrder list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
const todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
const task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
@@ -930,8 +940,6 @@ describe('cron', () => {
|
||||
});
|
||||
|
||||
it('should preserve todos order in task list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
const todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
import highlightMentions from '../../../../website/server/libs/highlightMentions';
|
||||
|
||||
describe('highlightMentions', () => {
|
||||
@@ -64,6 +65,67 @@ describe('highlightMentions', () => {
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
describe('link interactions', async () => {
|
||||
it('doesn\'t highlight users in link', async () => {
|
||||
const text = 'http://www.medium.com/@user/blog';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight user in link between brackets', async () => {
|
||||
const text = '(http://www.medium.com/@user/blog)';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight user in an autolink', async () => {
|
||||
const text = '<http://www.medium.com/@user/blog>';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight users in link text', async () => {
|
||||
const text = '[Check awesome blog written by @user](http://www.medium.com/@user/blog)';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight users in link with newlines and markup', async () => {
|
||||
const text = '[Check `awesome` \nblog **written** by @user](http://www.medium.com/@user/blog)';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
it('doesn\'t highlight users in link when followed by same @user mention', async () => {
|
||||
const text = 'http://www.medium.com/@user/blog @user';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('http://www.medium.com/@user/blog [@user](/profile/111)');
|
||||
});
|
||||
|
||||
// https://spec.commonmark.org/0.29/#example-483
|
||||
it('doesn\'t highlight user in a link without url', async () => {
|
||||
const text = '[@user2]()';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
// https://github.com/HabitRPG/habitica/issues/12217
|
||||
it('doesn\'t highlight user in link with url-escapable characters', async () => {
|
||||
const text = '[test](https://habitica.fandom.com/ru/@wiki/Снаряжение)';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal(text);
|
||||
});
|
||||
|
||||
// https://github.com/HabitRPG/habitica/issues/12223
|
||||
it('matches a link in between two the same links', async () => {
|
||||
const text = '[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n@user\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)';
|
||||
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
expect(result[0]).to.equal('[here](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)\n[@user](/profile/111)\n[hier](http://habitica.wikia.com/wiki/The_Keep:Pirate_Cove/FAQ)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('exceptions in code blocks', () => {
|
||||
it('doesn\'t highlight user in inline code block', async () => {
|
||||
const text = '`@user`';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import moment from 'moment';
|
||||
import { calculateSubscriptionTerminationDate } from '../../../../../../website/server/libs/payments/util';
|
||||
import calculateSubscriptionTerminationDate from '../../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate';
|
||||
import api from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
const groupPlanId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
|
||||
describe('#calculateSubscriptionTerminationDate', () => {
|
||||
let plan;
|
||||
let nextBill;
|
||||
@@ -13,52 +15,75 @@ describe('#calculateSubscriptionTerminationDate', () => {
|
||||
};
|
||||
nextBill = moment();
|
||||
});
|
||||
|
||||
it('should extend date to the exact amount of days left before the next bill will occur', () => {
|
||||
nextBill = moment()
|
||||
.add(5, 'days');
|
||||
const expectedTerminationDate = moment()
|
||||
.add(5, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('if nextBill is null, add 30 days to termination date', () => {
|
||||
nextBill = null;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(30, 'days');
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('if nextBill is null and it\'s a group plan, add 2 days instead of 30', () => {
|
||||
nextBill = null;
|
||||
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(2, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('should add 30.5 days for each extraMonth', () => {
|
||||
plan.extraMonths = 4;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(30.5 * 4, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('should round up if total days gained by extraMonth is a decimal number', () => {
|
||||
plan.extraMonths = 5;
|
||||
const expectedTerminationDate = moment()
|
||||
.add(Math.ceil(30.5 * 5), 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('behaves like extraMonths is 0 if it\'s set to a negative number', () => {
|
||||
plan.extraMonths = -5;
|
||||
const expectedTerminationDate = moment();
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, api.constants);
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
|
||||
it('returns current terminated date if it exists and is later than newly calculated date', () => {
|
||||
const expectedTerminationDate = moment().add({ months: 5 }).toDate();
|
||||
plan.dateTerminated = expectedTerminationDate;
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
|
||||
expect(terminationDate).to.equal(expectedTerminationDate);
|
||||
});
|
||||
|
||||
it('returns the calculated termination date if the plan does not have one', () => {
|
||||
nextBill = moment().add(5, 'days');
|
||||
const expectedTerminationDate = moment().add(5, 'days');
|
||||
|
||||
const terminationDate = calculateSubscriptionTerminationDate(nextBill, plan, groupPlanId);
|
||||
expect(expectedTerminationDate.diff(terminationDate, 'days')).to.eql(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getMatchesByWordArray } from '../../../../website/server/libs/stringUtils';
|
||||
import bannedWords from '../../../../website/server/libs/bannedWords';
|
||||
|
||||
describe('stringUtils', () => {
|
||||
describe('getMatchesByWordArray', () => {
|
||||
it('check all banned words are matched', async () => {
|
||||
const message = bannedWords.join(',').replace(/\\/g, '');
|
||||
const matches = getMatchesByWordArray(message, bannedWords);
|
||||
expect(matches.length).to.equal(bannedWords.length);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
TAVERN_ID,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
|
||||
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
|
||||
import bannedWords from '../../../../../website/server/libs/bannedWords';
|
||||
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
@@ -292,12 +290,6 @@ describe('POST /chat', () => {
|
||||
.that.includes(testBannedWords.join(', '));
|
||||
});
|
||||
|
||||
it('check all banned words are matched', async () => {
|
||||
const message = bannedWords.join(',').replace(/\\/g, '');
|
||||
const matches = getMatchesByWordArray(message, bannedWords);
|
||||
expect(matches.length).to.equal(bannedWords.length);
|
||||
});
|
||||
|
||||
it('does not error when bad word is suffix of a word', async () => {
|
||||
const wordAsSuffix = `prefix${testBannedWordMessage}`;
|
||||
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
generateChallenge,
|
||||
@@ -13,7 +11,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import payments from '../../../../../website/server/libs/payments/payments';
|
||||
import { calculateSubscriptionTerminationDate } from '../../../../../website/server/libs/payments/util';
|
||||
import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate';
|
||||
|
||||
describe('POST /groups/:groupId/leave', () => {
|
||||
const typesOfGroups = {
|
||||
@@ -359,6 +357,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
'purchased.plan.extraMonths': extraMonths,
|
||||
});
|
||||
});
|
||||
|
||||
it('calculates dateTerminated and sets extraMonths to zero after user leaves the group', async () => {
|
||||
const userBeforeLeave = await User.findById(member._id).exec();
|
||||
|
||||
@@ -373,7 +372,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
const expectedTerminationDate = calculateSubscriptionTerminationDate(null, {
|
||||
customerId: payments.constants.GROUP_PLAN_CUSTOMER_ID,
|
||||
extraMonths,
|
||||
}, payments.constants);
|
||||
}, payments.constants.GROUP_PLAN_CUSTOMER_ID);
|
||||
|
||||
expect(extraMonthsBefore).to.gte(12);
|
||||
expect(extraMonthsAfter).to.equal(0);
|
||||
|
||||
@@ -10,6 +10,9 @@ describe('GET /hall/heroes', () => {
|
||||
const nonHero = await generateUser();
|
||||
const hero1 = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
secret: {
|
||||
text: 'Super-Hero',
|
||||
},
|
||||
});
|
||||
const hero2 = await generateUser({
|
||||
contributor: { level: 3 },
|
||||
@@ -21,6 +24,8 @@ describe('GET /hall/heroes', () => {
|
||||
expect(heroes[1]._id).to.equal(hero1._id);
|
||||
|
||||
expect(heroes[0]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']);
|
||||
|
||||
// should not contain the secret
|
||||
expect(heroes[1]).to.have.all.keys(['_id', 'contributor', 'backer', 'profile']);
|
||||
|
||||
expect(heroes[0].profile).to.have.all.keys(['name']);
|
||||
@@ -28,5 +33,6 @@ describe('GET /hall/heroes', () => {
|
||||
|
||||
expect(heroes[0].profile.name).to.equal(hero2.profile.name);
|
||||
expect(heroes[1].profile.name).to.equal(hero1.profile.name);
|
||||
expect(heroes[1].secret).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,15 +43,19 @@ describe('GET /heroes/:heroId', () => {
|
||||
it('returns only necessary hero data given user id', async () => {
|
||||
const hero = await generateUser({
|
||||
contributor: { tier: 23 },
|
||||
secret: {
|
||||
text: 'Super Hero',
|
||||
},
|
||||
});
|
||||
const heroRes = await user.get(`/hall/heroes/${hero._id}`);
|
||||
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items',
|
||||
'contributor', 'auth', 'items', 'secret',
|
||||
]);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
expect(heroRes.secret.text).to.be.eq('Super Hero');
|
||||
});
|
||||
|
||||
it('returns only necessary hero data given username', async () => {
|
||||
@@ -62,7 +66,7 @@ describe('GET /heroes/:heroId', () => {
|
||||
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items',
|
||||
'contributor', 'auth', 'items', 'secret',
|
||||
]);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
describe('PUT /heroes/:heroId', () => {
|
||||
let user;
|
||||
|
||||
const heroFields = [
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
'secret',
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser({
|
||||
contributor: { admin: true },
|
||||
@@ -51,10 +57,8 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
]);
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
@@ -130,10 +134,8 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
]);
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
@@ -157,10 +159,8 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
]);
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
@@ -173,6 +173,40 @@ describe('PUT /heroes/:heroId', () => {
|
||||
expect(hero.contributor.text).to.equal('Astronaut');
|
||||
});
|
||||
|
||||
it('updates contributor secret', async () => {
|
||||
const secretText = 'my super hero';
|
||||
|
||||
const hero = await generateUser({
|
||||
contributor: { level: 5 },
|
||||
secret: {
|
||||
text: 'supr hro typo',
|
||||
},
|
||||
});
|
||||
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
contributor: { text: 'Astronaut' },
|
||||
secret: {
|
||||
text: secretText,
|
||||
},
|
||||
});
|
||||
|
||||
// test response
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
// test response values
|
||||
expect(heroRes.contributor.level).to.equal(5); // doesn't modify previous values
|
||||
expect(heroRes.contributor.text).to.equal('Astronaut');
|
||||
expect(heroRes.secret.text).to.equal(secretText);
|
||||
|
||||
// test hero values
|
||||
await hero.sync();
|
||||
expect(hero.contributor.level).to.equal(5); // doesn't modify previous values
|
||||
expect(hero.contributor.text).to.equal('Astronaut');
|
||||
expect(hero.secret.text).to.equal(secretText);
|
||||
});
|
||||
|
||||
it('updates items', async () => {
|
||||
const hero = await generateUser();
|
||||
const heroRes = await user.put(`/hall/heroes/${hero._id}`, {
|
||||
@@ -181,10 +215,8 @@ describe('PUT /heroes/:heroId', () => {
|
||||
});
|
||||
|
||||
// test response
|
||||
expect(heroRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
'_id', 'balance', 'profile', 'purchased',
|
||||
'contributor', 'auth', 'items', 'flags',
|
||||
]);
|
||||
// works as: object has all and only these keys
|
||||
expect(heroRes).to.have.all.keys(heroFields);
|
||||
expect(heroRes.auth.local).not.to.have.keys(['salt', 'hashed_password']);
|
||||
expect(heroRes.profile).to.have.all.keys(['name']);
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ describe('GET /members/:memberId', () => {
|
||||
costume: false,
|
||||
background: 'volcano',
|
||||
},
|
||||
secret: {
|
||||
text: 'Clark Kent',
|
||||
},
|
||||
});
|
||||
const memberRes = await user.get(`/members/${member._id}`);
|
||||
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
|
||||
@@ -46,6 +49,29 @@ describe('GET /members/:memberId', () => {
|
||||
expect(memberRes.stats.toNextLevel).to.equal(common.tnl(memberRes.stats.lvl));
|
||||
expect(memberRes.inbox.optOut).to.exist;
|
||||
expect(memberRes.inbox.messages).to.not.exist;
|
||||
expect(memberRes.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not return secret for the own account', 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 member.get(`/members/${member._id}`);
|
||||
expect(memberRes).to.have.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(memberRes.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import range from 'lodash/range';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
@@ -26,6 +27,7 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
expect(returnedUser.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns only user properties requested', async () => {
|
||||
@@ -38,4 +40,30 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.notifications).to.exist;
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not return requested private properties', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
||||
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
expect(returnedUser.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns the full inbox', async () => {
|
||||
const otherUser = await generateUser();
|
||||
const amountOfMessages = 12;
|
||||
|
||||
const allMessagesPromise = range(amountOfMessages)
|
||||
.map(i => otherUser.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: `Message Num: ${i}`,
|
||||
}));
|
||||
|
||||
await Promise.all(allMessagesPromise);
|
||||
|
||||
const returnedUser = await user.get('/user');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.inbox).to.exist;
|
||||
expect(Object.keys(returnedUser.inbox.messages)).to.have.a.lengthOf(amountOfMessages);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,11 @@ describe('GET /user/anonymized', () => {
|
||||
const endpoint = '/user/anonymized';
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
user = await generateUser({
|
||||
secret: {
|
||||
text: 'Clark Kent',
|
||||
},
|
||||
});
|
||||
await user.update({
|
||||
newMessages: ['some', 'new', 'messages'],
|
||||
'profile.name': 'profile',
|
||||
@@ -100,5 +104,7 @@ describe('GET /user/anonymized', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
expect(returnedUser.secret).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,4 +117,24 @@ describe('POST /user/reset', () => {
|
||||
expect(userChallengeTask).to.exist;
|
||||
expect(syncedGroupTask).to.exist;
|
||||
});
|
||||
|
||||
it('does not delete secret', async () => {
|
||||
const admin = await generateUser({
|
||||
contributor: { admin: true },
|
||||
});
|
||||
|
||||
const hero = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
secret: {
|
||||
text: 'Super-Hero',
|
||||
},
|
||||
});
|
||||
|
||||
await hero.post('/user/reset');
|
||||
|
||||
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);
|
||||
|
||||
expect(heroRes.secret).to.exist;
|
||||
expect(heroRes.secret.text).to.be.eq('Super-Hero');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
describe('POST /user/unlock', () => {
|
||||
let user;
|
||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||
const unlockCost = 1.25;
|
||||
const usersStartingGems = 5;
|
||||
|
||||
@@ -34,4 +35,25 @@ describe('POST /user/unlock', () => {
|
||||
expect(response.message).to.equal(t('unlocked'));
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
});
|
||||
|
||||
it('does not reduce a user\'s balance twice', async () => {
|
||||
await user.update({
|
||||
balance: usersStartingGems,
|
||||
});
|
||||
const response = await user.post(`/user/unlock?path=${unlockGearSetPath}`);
|
||||
await user.sync();
|
||||
|
||||
expect(response.message).to.equal(t('unlocked'));
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
|
||||
expect(user.post(`/user/unlock?path=${unlockGearSetPath}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('alreadyUnlocked'),
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ describe('PUT /user', () => {
|
||||
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },
|
||||
notifications: [{ type: 123 }],
|
||||
webhooks: { webhooks: [{ url: 'https://foobar.com' }] },
|
||||
secret: { secret: { text: 'Some new text' } },
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
@@ -129,6 +130,7 @@ describe('PUT /user', () => {
|
||||
webhooks: { 'preferences.webhooks': [1, 2, 3] },
|
||||
sleep: { 'preferences.sleep': true },
|
||||
'disable classes': { 'preferences.disableClasses': true },
|
||||
secret: { secret: { text: 'Some new text' } },
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('GET /inbox/conversations', () => {
|
||||
expect(messages[4].text).to.equal('first');
|
||||
});
|
||||
|
||||
it('returns four messages when using page-query ', async () => {
|
||||
it('returns five messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.auth.local.passwordHashMethod).to.not.exist;
|
||||
expect(returnedUser.auth.local.salt).to.not.exist;
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
expect(returnedUser.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns only user properties requested', async () => {
|
||||
@@ -39,6 +40,13 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not return requested private properties', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
||||
|
||||
expect(returnedUser.apiToken).to.not.exist;
|
||||
expect(returnedUser.secret).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not return new inbox messages', async () => {
|
||||
const otherUser = await generateUser();
|
||||
|
||||
|
||||
@@ -117,4 +117,25 @@ describe('POST /user/reset', () => {
|
||||
expect(userChallengeTask).to.exist;
|
||||
expect(syncedGroupTask).to.exist;
|
||||
});
|
||||
|
||||
it('does not delete secret', async () => {
|
||||
const admin = await generateUser({
|
||||
contributor: { admin: true },
|
||||
});
|
||||
|
||||
const hero = await generateUser({
|
||||
contributor: { level: 1 },
|
||||
secret: {
|
||||
text: 'Super-Hero',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
await hero.post('/user/reset');
|
||||
|
||||
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);
|
||||
|
||||
expect(heroRes.secret).to.exist;
|
||||
expect(heroRes.secret.text).to.be.eq('Super-Hero');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,7 @@ describe('PUT /user', () => {
|
||||
'customization gem purchases': { 'purchased.background.tavern': true, 'purchased.skin.bear': true },
|
||||
notifications: [{ type: 123 }],
|
||||
webhooks: { webhooks: [{ url: 'https://foobar.com' }] },
|
||||
secret: { secret: { text: 'Some new text' } },
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
@@ -113,6 +114,7 @@ describe('PUT /user', () => {
|
||||
webhooks: { 'preferences.webhooks': [1, 2, 3] },
|
||||
sleep: { 'preferences.sleep': true },
|
||||
'disable classes': { 'preferences.disableClasses': true },
|
||||
secret: { secret: { text: 'Some new text' } },
|
||||
};
|
||||
|
||||
each(protectedOperations, (data, testName) => {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"test": {
|
||||
plugins: [
|
||||
["istanbul"],
|
||||
],
|
||||
},
|
||||
},
|
||||
"presets": [
|
||||
["es2015", { modules: false }],
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
],
|
||||
"comments": false,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// TODO verify if it's needed, added because Axios require Promise in the global scope
|
||||
// and babel-runtime doesn't affect external libraries
|
||||
require('babel-polyfill');
|
||||
|
||||
// Automatically setup SinonJS' sandbox for each test
|
||||
beforeEach(() => {
|
||||
global.sandbox = sinon.createSandbox();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.sandbox.restore();
|
||||
});
|
||||
|
||||
// require all test files
|
||||
const testsContext = require.context('./specs', true, /\.js$/);
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
||||
// require all .vue and .js files except main.js for coverage.
|
||||
const srcContext = require.context('../../../website/client', true, /^\.\/(?=(?!main(\.js)?$))(?=(.*\.(vue|js)$))/);
|
||||
srcContext.keys().forEach(srcContext);
|
||||
@@ -1,40 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// This is a karma config file. For more details see
|
||||
// http://karma-runner.github.io/0.13/config/configuration-file.html
|
||||
// we are also using it with karma-webpack
|
||||
// https://github.com/webpack/karma-webpack
|
||||
|
||||
// Necessary for babel to respect the env version of .babelrc which is necessary
|
||||
// Because inject-loader does not work with ["es2015", { modules: false }] that we use
|
||||
// in order to let webpack2 handle the imports
|
||||
process.env.CHROME_BIN = require('puppeteer').executablePath();
|
||||
// eslint-disable-line no-process-env
|
||||
process.env.BABEL_ENV = process.env.NODE_ENV; // eslint-disable-line no-process-env
|
||||
const webpackConfig = require('../../../webpack/webpack.test.conf');
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// to run in additional browsers:
|
||||
// 1. install corresponding karma launcher
|
||||
// http://karma-runner.github.io/0.13/config/browsers.html
|
||||
// 2. add it to the `browsers` array below.
|
||||
browsers: ['ChromeHeadless'],
|
||||
frameworks: ['mocha', 'sinon-stub-promise', 'sinon-chai', 'chai-as-promised', 'chai'],
|
||||
reporters: ['spec', 'coverage'],
|
||||
files: ['./index.js'],
|
||||
preprocessors: {
|
||||
'./index.js': ['webpack', 'sourcemap'],
|
||||
},
|
||||
webpack: webpackConfig,
|
||||
webpackMiddleware: {
|
||||
noInfo: true,
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: '../../../coverage/client-unit',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' },
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -119,7 +119,7 @@ describe('shared.ops.buyGem', () => {
|
||||
buyGem(user, { params: { type: 'gems', key: 'gem' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('reachedGoldToGemCap', { convCap: planGemLimits.convCap }));
|
||||
expect(err.message).to.equal(i18n.t('maxBuyGems', { convCap: planGemLimits.convCap }));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +88,14 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
expect(user.items.gear.owned).to.have.property('armor_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('head_mystery_301404', true);
|
||||
expect(user.items.gear.owned).to.have.property('eyewear_mystery_301404', true);
|
||||
expect(analytics.track).to.be.called;
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWithMatch('acquire item', {
|
||||
uuid: user._id,
|
||||
itemKey: '301404',
|
||||
itemType: 'Subscriber Gear',
|
||||
acquireMethod: 'Hourglass',
|
||||
category: 'behavior',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,6 +52,22 @@ describe('shared.ops.reset', () => {
|
||||
expect(user.stats.lvl).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets user\'s stat points (str, con, int, per, points)', () => {
|
||||
user.stats.str = 2;
|
||||
user.stats.con = 2;
|
||||
user.stats.int = 2;
|
||||
user.stats.per = 2;
|
||||
user.stats.points = 2;
|
||||
|
||||
reset(user);
|
||||
|
||||
expect(user.stats.str).to.equal(0);
|
||||
expect(user.stats.con).to.equal(0);
|
||||
expect(user.stats.int).to.equal(0);
|
||||
expect(user.stats.per).to.equal(0);
|
||||
expect(user.stats.points).to.equal(1);
|
||||
});
|
||||
|
||||
it('resets user\'s gold', () => {
|
||||
user.stats.gp = 20;
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
import get from 'lodash/get';
|
||||
import unlock from '../../../website/common/script/ops/unlock';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import { generateUser } from '../../helpers/common.helper';
|
||||
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
||||
|
||||
describe('shared.ops.unlock', () => {
|
||||
let user;
|
||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||
const backgroundUnlockPath = 'background.giant_florals';
|
||||
const unlockCost = 1.25;
|
||||
const usersStartingGems = 5;
|
||||
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
|
||||
const usersStartingGems = 50 / 4;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
@@ -31,6 +27,15 @@ describe('shared.ops.unlock', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not unlock lost gear', done => {
|
||||
user.items.gear.owned.headAccessory_special_bearEars = false;
|
||||
|
||||
unlock(user, { query: { path: 'items.gear.owned.headAccessory_special_bearEars' } });
|
||||
|
||||
expect(user.balance).to.equal(usersStartingGems);
|
||||
done();
|
||||
});
|
||||
|
||||
it('returns an error when user balance is too low', done => {
|
||||
user.balance = 0;
|
||||
|
||||
@@ -44,28 +49,124 @@ describe('shared.ops.unlock', () => {
|
||||
});
|
||||
|
||||
it('returns an error when user already owns a full set', done => {
|
||||
let expectedBalance;
|
||||
|
||||
try {
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
expectedBalance = user.balance;
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||
expect(user.balance).to.equal(expectedBalance);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
// disabled until fully implemente
|
||||
xit('returns an error when user already owns items in a full set', done => {
|
||||
it('returns an error when user already owns a full set of gear', done => {
|
||||
let expectedBalance;
|
||||
|
||||
try {
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
unlock(user, { query: { path: unlockGearSetPath } });
|
||||
expectedBalance = user.balance;
|
||||
unlock(user, { query: { path: unlockGearSetPath } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlocked'));
|
||||
expect(user.balance).to.equal(expectedBalance);
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error if an item does not exists', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: 'background.invalid_background' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error if there are items from multiple sets', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: 'shirt.convict,skin.0ff591' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error if gear is not from the animal set', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error if the item is free', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: 'shirt.black' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error if an item does not belong to a set (appearances)', done => {
|
||||
try {
|
||||
unlock(user, { query: { path: 'shirt.pink' } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns an error when user already owns items in a full set and it would be more expensive to buy the entire set', done => {
|
||||
try {
|
||||
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
||||
// In order for the full purchase not to be worth, we must own 9
|
||||
const partialUnlockPaths = unlockPath.split(',');
|
||||
unlock(user, { query: { path: partialUnlockPaths[0] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[1] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[2] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[3] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[8] } });
|
||||
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('alreadyUnlockedPart'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not return an error when user already owns items in a full set and it would not be more expensive to buy the entire set', () => {
|
||||
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
||||
// In order for the full purchase to be worth, we can own already 8
|
||||
const partialUnlockPaths = unlockPath.split(',');
|
||||
unlock(user, { query: { path: partialUnlockPaths[0] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[1] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[2] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[3] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[4] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[5] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[6] } });
|
||||
unlock(user, { query: { path: partialUnlockPaths[7] } });
|
||||
|
||||
unlock(user, { query: { path: unlockPath } });
|
||||
});
|
||||
|
||||
it('equips an item already owned', () => {
|
||||
expect(user.purchased.background.giant_florals).to.not.exist;
|
||||
|
||||
@@ -78,7 +179,7 @@ describe('shared.ops.unlock', () => {
|
||||
expect(user.preferences.background).to.equal('giant_florals');
|
||||
});
|
||||
|
||||
it('un-equips an item already equipped', () => {
|
||||
it('un-equips a background already equipped', () => {
|
||||
expect(user.purchased.background.giant_florals).to.not.exist;
|
||||
|
||||
unlock(user, { query: { path: backgroundUnlockPath } }); // unlock
|
||||
@@ -91,31 +192,78 @@ describe('shared.ops.unlock', () => {
|
||||
expect(user.preferences.background).to.equal('');
|
||||
});
|
||||
|
||||
it('unlocks a full set', () => {
|
||||
it('unlocks a full set of appearance items', () => {
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = unlock(user, { query: { path: unlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(user.purchased.shirt.convict).to.be.true;
|
||||
const individualPaths = unlockPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.shirt).length)
|
||||
.to.equal(initialShirts + individualPaths.length);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
it('unlocks a full set of gear', () => {
|
||||
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||
const [, message] = unlock(user, { query: { path: unlockGearSetPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(user.items.gear.owned.headAccessory_special_wolfEars).to.be.true;
|
||||
|
||||
const individualPaths = unlockGearSetPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.items.gear.owned).length)
|
||||
.to.equal(initialGear + individualPaths.length);
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||
});
|
||||
|
||||
it('unlocks a an item', () => {
|
||||
it('unlocks a full set of backgrounds', () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
const [, message] = unlock(user, { query: { path: backgroundSetUnlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
const individualPaths = backgroundSetUnlockPath.split(',');
|
||||
individualPaths.forEach(path => {
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
});
|
||||
expect(Object.keys(user.purchased.background).length)
|
||||
.to.equal(initialBackgrounds + individualPaths.length);
|
||||
expect(user.balance).to.equal(usersStartingGems - 3.75);
|
||||
});
|
||||
|
||||
it('unlocks an item (appearance)', () => {
|
||||
const path = unlockPath.split(',')[0];
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = unlock(user, { query: { path } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
|
||||
it('unlocks an item (gear)', () => {
|
||||
const path = unlockGearSetPath.split(',')[0];
|
||||
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||
const [, message] = unlock(user, { query: { path } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(Object.keys(user.items.gear.owned).length).to.equal(initialGear + 1);
|
||||
expect(get(user, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
});
|
||||
|
||||
it('unlocks an item (background)', () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
const [, message] = unlock(user, { query: { path: backgroundUnlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(user.purchased.background.giant_florals).to.be.true;
|
||||
});
|
||||
|
||||
it('reduces a user\'s balance', () => {
|
||||
const [, message] = unlock(user, { query: { path: unlockPath } });
|
||||
|
||||
expect(message).to.equal(i18n.t('unlocked'));
|
||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
||||
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
|
||||
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,46 +5,46 @@
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
|
||||
"lint": "vue-cli-service lint .",
|
||||
"lint-no-fix": "vue-cli-service lint --no-fix .",
|
||||
"postinstall": "node ./scripts/npm-postinstall.js",
|
||||
"storybook:build": "vue-cli-service storybook:build -c config/storybook -o dist/storybook",
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook",
|
||||
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js"
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.3.1",
|
||||
"@vue/cli-plugin-eslint": "^4.3.1",
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.3.1",
|
||||
"@vue/cli-service": "^4.3.1",
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
"@storybook/addon-knobs": "^5.3.18",
|
||||
"@storybook/addon-links": "^5.3.18",
|
||||
"@storybook/addon-notes": "^5.3.18",
|
||||
"@storybook/vue": "^5.3.18",
|
||||
"@storybook/addon-actions": "^5.3.19",
|
||||
"@storybook/addon-knobs": "^5.3.19",
|
||||
"@storybook/addon-links": "^5.3.19",
|
||||
"@storybook/addon-notes": "^5.3.19",
|
||||
"@storybook/vue": "^5.3.19",
|
||||
"@vue/cli-plugin-babel": "^4.4.1",
|
||||
"@vue/cli-plugin-eslint": "^4.4.1",
|
||||
"@vue/cli-plugin-router": "^4.4.1",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.4.1",
|
||||
"@vue/cli-service": "^4.4.1",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^5.11.0",
|
||||
"axios": "^0.19.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.4.1",
|
||||
"bootstrap-vue": "^2.11.0",
|
||||
"bootstrap": "^4.5.0",
|
||||
"bootstrap-vue": "^2.15.0",
|
||||
"chai": "^4.1.2",
|
||||
"core-js": "^3.6.5",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^1.3.2",
|
||||
"habitica-markdown": "^2.0.0",
|
||||
"hellojs": "^1.18.4",
|
||||
"inspectpack": "^4.4.0",
|
||||
"inspectpack": "^4.5.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": "^3.5.0",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"moment": "^2.26.0",
|
||||
"nconf": "^0.10.0",
|
||||
"sass": "^1.26.3",
|
||||
"sass": "^1.26.8",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.15.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
@@ -56,10 +56,10 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-cli-plugin-storybook": "^0.6.1",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-router": "^3.3.2",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"vuedraggable": "^2.23.1",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^4.42.1"
|
||||
"webpack": "^4.43.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<meta name="smartbanner:button-url-apple" content="https://itunes.apple.com/us/app/habitica-gamified-taskmanager/id994882113">
|
||||
<meta name="smartbanner:button-url-google" content="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica">
|
||||
<meta name="smartbanner:enabled-platforms" content="android,ios">
|
||||
<meta name="smartbanner:hide-ttl" content="2592000000">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
||||
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
|
||||
@@ -297,7 +297,7 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded']),
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
@@ -361,13 +361,55 @@ export default {
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
// Set up Error interceptors
|
||||
axios.interceptors.response.use(response => {
|
||||
if (this.user && response.data && response.data.notifications) {
|
||||
this.$set(this.user, 'notifications', response.data.notifications);
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
// If it was, sync
|
||||
const { url } = response.config;
|
||||
const { method } = response.config;
|
||||
|
||||
const isApiCall = url.indexOf('api/v4') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
this.user._v = userV;
|
||||
|
||||
// Do not sync again if already syncing
|
||||
const isUserSync = url.indexOf('/api/v4/user') === 0 && method === 'get';
|
||||
const isTasksSync = url.indexOf('/api/v4/tasks/user') === 0 && method === 'get';
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v4/cron because the user is synced automatically after cron runs
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Store the app version from the server
|
||||
const serverAppVersion = response.data && response.data.appVersion;
|
||||
|
||||
if (serverAppVersion && this.$store.state.serverAppVersion !== response.data.appVersion) {
|
||||
this.$store.state.serverAppVersion = serverAppVersion;
|
||||
}
|
||||
|
||||
// Store the notifications, filtering those that have already been read
|
||||
// See store/index.js on why this is necessary
|
||||
if (this.user && response.data && response.data.notifications) {
|
||||
const filteredNotifications = response.data.notifications.filter(serverNotification => {
|
||||
if (this.notificationsRemoved.includes(serverNotification.id)) return false;
|
||||
return true;
|
||||
});
|
||||
this.$set(this.user, 'notifications', filteredNotifications);
|
||||
}
|
||||
|
||||
return response;
|
||||
}, error => {
|
||||
}, error => { // Set up Error interceptors
|
||||
if (error.response.status >= 400) {
|
||||
const isBanned = this.checkForBannedUser(error);
|
||||
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
||||
@@ -425,51 +467,6 @@ export default {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => {
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
// If it was, sync
|
||||
const { url } = response.config;
|
||||
const { method } = response.config;
|
||||
|
||||
const isApiCall = url.indexOf('api/v4') !== -1;
|
||||
const userV = response.data && response.data.userV;
|
||||
const isCron = url.indexOf('/api/v4/cron') === 0 && method === 'post';
|
||||
|
||||
if (this.isUserLoaded && isApiCall && userV) {
|
||||
const oldUserV = this.user._v;
|
||||
this.user._v = userV;
|
||||
|
||||
// Do not sync again if already syncing
|
||||
const isUserSync = url.indexOf('/api/v4/user') === 0 && method === 'get';
|
||||
const isTasksSync = url.indexOf('/api/v4/tasks/user') === 0 && method === 'get';
|
||||
// exclude chat seen requests because with real time chat they would be too many
|
||||
const isChatSeen = url.indexOf('/chat/seen') !== -1 && method === 'post';
|
||||
// exclude POST /api/v4/cron because the user is synced automatically after cron runs
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (userV - oldUserV > 1 && !isCron && !isChatSeen && !isUserSync && !isTasksSync) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', { forceLoad: true }),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the client is updated
|
||||
// const serverAppVersion = response.data.appVersion;
|
||||
// let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
// if (isApiCall && !serverAppVersionState) {
|
||||
// this.$store.state.serverAppVersion = serverAppVersion;
|
||||
// } else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
// if (document.activeElement.tagName !== 'INPUT'
|
||||
// || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
// location.reload(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
// Setup listener for title
|
||||
this.$store.watch(state => state.title, title => {
|
||||
document.title = title;
|
||||
|
||||
@@ -1,96 +1,24 @@
|
||||
.promo_april_fools_2020 {
|
||||
.promo_armoire_backgrounds_202006 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -445px -184px;
|
||||
background-position: -259px 0px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_armoire_backgrounds_202004 {
|
||||
.promo_mystery_202006 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -433px -337px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_egg_quest {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -648px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_mystery_202004 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -796px;
|
||||
background-position: 0px -259px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_pastel_skin_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -355px -648px;
|
||||
width: 354px;
|
||||
height: 147px;
|
||||
}
|
||||
.customize-option.promo_pastel_skin_hair {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -380px -663px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.promo_seasonal_shop_spring {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -875px -524px;
|
||||
width: 162px;
|
||||
height: 138px;
|
||||
}
|
||||
.promo_shiny_seeds {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -424px -500px;
|
||||
width: 360px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_spring_2019 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -337px;
|
||||
width: 432px;
|
||||
height: 162px;
|
||||
}
|
||||
.promo_spring_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -445px 0px;
|
||||
width: 429px;
|
||||
height: 183px;
|
||||
}
|
||||
.promo_spring_potions_2020 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px -500px;
|
||||
width: 423px;
|
||||
height: 147px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -875px -663px;
|
||||
background-position: -259px -148px;
|
||||
width: 96px;
|
||||
height: 69px;
|
||||
}
|
||||
.scene_citrusella {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -875px -196px;
|
||||
width: 152px;
|
||||
height: 176px;
|
||||
}
|
||||
.scene_hat_guild {
|
||||
.scene_hiking {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: 0px 0px;
|
||||
width: 444px;
|
||||
height: 336px;
|
||||
}
|
||||
.scene_meditation {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -875px -373px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.scene_tasks {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||
background-position: -875px 0px;
|
||||
width: 240px;
|
||||
height: 195px;
|
||||
width: 258px;
|
||||
height: 258px;
|
||||
}
|
||||
|
||||
@@ -1,72 +1,120 @@
|
||||
.quest_TEMPLATE_FOR_MISSING_IMAGE {
|
||||
.quest_bunny {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -502px -1519px;
|
||||
width: 221px;
|
||||
height: 39px;
|
||||
background-position: 0px -1546px;
|
||||
width: 210px;
|
||||
height: 186px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
.quest_butterfly {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px 0px;
|
||||
background-position: -1320px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dustbunnies {
|
||||
.quest_cheetah {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_egg {
|
||||
.quest_cow {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -362px;
|
||||
width: 165px;
|
||||
height: 207px;
|
||||
background-position: -1760px 0px;
|
||||
width: 174px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -1023px;
|
||||
width: 118px;
|
||||
height: 131px;
|
||||
}
|
||||
.quest_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_ferret {
|
||||
.quest_dilatory {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dilatoryDistress1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -222px -1332px;
|
||||
width: 210px;
|
||||
height: 210px;
|
||||
}
|
||||
.quest_dilatoryDistress2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -422px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_dilatoryDistress3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dilatory_derby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dustbunnies {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_egg {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -214px;
|
||||
width: 165px;
|
||||
height: 207px;
|
||||
}
|
||||
.quest_evilsanta {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1760px -875px;
|
||||
width: 118px;
|
||||
height: 131px;
|
||||
}
|
||||
.quest_evilsanta2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_falcon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_ferret {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_frog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px 0px;
|
||||
background-position: 0px -1332px;
|
||||
width: 221px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_ghost_stag {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px 0px;
|
||||
background-position: -440px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -220px;
|
||||
background-position: -660px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_goldenknight2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1519px;
|
||||
background-position: -462px -1546px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
@@ -78,319 +126,271 @@
|
||||
}
|
||||
.quest_gryphon {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -443px -1332px;
|
||||
background-position: -876px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_guineapig {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -452px;
|
||||
background-position: -1100px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_harpy {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -452px;
|
||||
background-position: -1100px -660px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_hedgehog {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1332px;
|
||||
background-position: -433px -1332px;
|
||||
width: 219px;
|
||||
height: 186px;
|
||||
}
|
||||
.quest_hippo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -452px;
|
||||
background-position: -220px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_horse {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -452px;
|
||||
background-position: -440px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kangaroo {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px 0px;
|
||||
background-position: -660px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_kraken {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -1332px;
|
||||
background-position: -1093px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_lostMasterclasser1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -220px;
|
||||
background-position: -1100px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -440px;
|
||||
background-position: -1320px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_lostMasterclasser3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -672px;
|
||||
background-position: -1320px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -570px;
|
||||
background-position: -1760px -724px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_mayhemMistiflying2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -672px;
|
||||
background-position: -220px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_mayhemMistiflying3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -672px;
|
||||
background-position: -1320px -880px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_monkey {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -672px;
|
||||
background-position: 0px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -214px;
|
||||
background-position: -1540px -437px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_moon2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -672px;
|
||||
background-position: -440px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moon3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px 0px;
|
||||
background-position: -660px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -220px;
|
||||
background-position: -880px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -440px;
|
||||
background-position: -1100px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_moonstone3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -660px;
|
||||
background-position: -1320px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_nudibranch {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -431px;
|
||||
background-position: -1540px -871px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_octopus {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -1332px;
|
||||
background-position: -653px -1332px;
|
||||
width: 222px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_owl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -892px;
|
||||
background-position: -1540px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_peacock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -648px;
|
||||
background-position: -1540px -220px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_penguin {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -178px;
|
||||
background-position: 0px -1733px;
|
||||
width: 190px;
|
||||
height: 183px;
|
||||
}
|
||||
.quest_pterodactyl {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -892px;
|
||||
background-position: -220px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rat {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -892px;
|
||||
background-position: -1320px -440px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_robot {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -892px;
|
||||
background-position: -880px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rock {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -865px;
|
||||
background-position: -1540px -654px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_rooster {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1528px -1332px;
|
||||
background-position: -1527px -1332px;
|
||||
width: 213px;
|
||||
height: 174px;
|
||||
}
|
||||
.quest_ruby {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -892px;
|
||||
background-position: 0px -892px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sabretooth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -892px;
|
||||
background-position: -1100px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_seaserpent {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px 0px;
|
||||
background-position: -880px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sheep {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -220px;
|
||||
background-position: -220px -672px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_silver {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -440px;
|
||||
background-position: -880px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_slime {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -660px;
|
||||
background-position: -660px -452px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_sloth {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -880px;
|
||||
background-position: -660px -220px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_snail {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1320px -1112px;
|
||||
background-position: -1540px -1088px;
|
||||
width: 219px;
|
||||
height: 213px;
|
||||
}
|
||||
.quest_snake {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -877px -1332px;
|
||||
background-position: -1310px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_spider {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -251px -1519px;
|
||||
background-position: -211px -1546px;
|
||||
width: 250px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_squirrel {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: 0px -1112px;
|
||||
background-position: -660px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -721px;
|
||||
background-position: -1760px -573px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_stoikalmCalamity2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -220px -1112px;
|
||||
background-position: 0px -232px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_stoikalmCalamity3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -440px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_taskwoodsTerror1 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px -872px;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
.quest_taskwoodsTerror2 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1540px -1082px;
|
||||
width: 216px;
|
||||
height: 216px;
|
||||
}
|
||||
.quest_taskwoodsTerror3 {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -660px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_treeling {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1094px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_trex {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1762px 0px;
|
||||
width: 204px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_trex_undead {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1311px -1332px;
|
||||
width: 216px;
|
||||
height: 177px;
|
||||
}
|
||||
.quest_triceratops {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -880px -1112px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_turtle {
|
||||
background-image: url('~@/assets/images/sprites/spritesmith-main-13.png');
|
||||
background-position: -1100px -1112px;
|
||||
background-position: -1100px 0px;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 470 KiB After Width: | Height: | Size: 470 KiB |
|
Before Width: | Height: | Size: 643 KiB After Width: | Height: | Size: 603 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 425 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 178 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 180 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 151 KiB |
@@ -4,23 +4,47 @@
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
padding: 4px 8px;
|
||||
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
.badge-pill {
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.badge-round {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.badge-default {
|
||||
background: $gray-500;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.badge-dialog {
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: -16px;
|
||||
|
||||
.badge-pin {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-item {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
}
|
||||
|
||||
.badge-top {
|
||||
position: absolute;
|
||||
left: calc((100% - 24px) / 2);
|
||||
top: -12px;
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
position: absolute;
|
||||
color: $white;
|
||||
|
||||