Compare commits

..

17 Commits

Author SHA1 Message Date
Phillip Thelen e10b7deb4b attempt implementing prerendering 2025-02-04 17:43:30 +01:00
Phillip Thelen d46fa98390 tell vite to ignore browser-script url 2025-02-04 17:42:55 +01:00
Phillip Thelen 3d6afa9f11 fix setupNconf imports 2025-02-04 12:29:33 +01:00
Phillip Thelen e7616cae8d setup nconf correctly for client build 2025-02-04 11:21:08 +01:00
Phillip Thelen 3a457f69a9 set serving url for vite content 2025-02-03 18:22:17 +01:00
Phillip Thelen a4297283cb fix lint 2025-02-03 17:45:09 +01:00
Phillip Thelen ca08e3ef81 fix import 2025-02-03 17:41:41 +01:00
Phillip Thelen c582dbd169 use vitest for client spec tests 2025-02-03 17:37:11 +01:00
Phillip Thelen 56ef07c8d2 fix various import issues 2025-02-03 16:05:03 +01:00
Phillip Thelen 896836f807 eslint fixes 2025-02-03 14:02:46 +01:00
Phillip Thelen b5458bb604 remove gulp-nodemon and update nodemon 2025-02-03 14:00:04 +01:00
Phillip Thelen 2edb255e55 move from vue-cli-service/webpack to vite 2025-01-23 15:11:10 +01:00
Rafał Jagielski 200d917582 Fix intro guide (#15247)
* Provide window.jquery to modules in vue.config.js

* Fix curly-spacing lint error
2025-01-22 14:53:24 -06:00
negue 73082a8cf0 fix toggle disable icon position 2025-01-21 22:34:34 +01:00
Kalista Payne dd08eee20c chore(images): update css 2025-01-21 08:52:21 -06:00
negue 895241b7fa show date tooltip above system / skill messages 2025-01-20 21:08:55 +01:00
negue 2535fd7095 Combined Message Pages/Redesign (#15310)
* split component prepare new views / states

* extract empty and disabled state as components

* fix empty state mail icon

* first logic switching between modes, move page to /private-messages/index.vue

* extract autoCompleteHelper.js

* style header + start new message input

* style plus button + focus input

* state logic, types for sanity

* WIP PM new Message started

* add /members/username test

* first design changes to messageCard

* delete private message or chat - based on the mode

* copy as todo

* mention links to modal

* report chat or private message

* WIP likeButton

* likeButton styling

* hide like on private message cards

* fix unit test

* replace copy as todo - to just a copy to clipboard

* style changes

* menu position + like button width

* dropdown items background + like font

* fix like button padding

* move api endpoints and tests around to group inbox methods  + like for inbox private messages

* restyle system messages

* Dropdown Radius and Padding

* WIP system messages

* fix lint

* copy delta commit of allowing liking own private messages

* enable liking private messages

* fix menu non hovered item icon color

* fix import path

* ignore background on system messages

* requested changes + migration

* update migration to update the unique id to some messages and delete the duplicates

* migration based on users pagination

* fix(migration): use Promise.all

* change to bulkWrites per User, and all messages in one run (of a user)

* check for array

* use rest operator ...

* skip sorting to get the users

* remove migration, disable like for private messages without uniqueMessageId

* lean+bulkWrite for likes, add time checks for like and auth for further debugging

* add a limit 2 get the messages by uniqueId

* Adding a simple server start script

* remove pinned nodemon dep

* fix inbox controller/tests

* fix / requested style changes

* fix empty state padding /

* hide avatar weapons on messages - fix avatar spacing on messages

* Hourglass Simplification (#15323)

* begin removing obsolete tests

* begin refactoring

* update cron tests

* cleanup

* finish basic implementation of new logic

* add more subscription tests

* subscription test improvements

* return nextHourglassDate again

* fix gem limit

* fix(test): short circuit this.

* fix(admin): correct logic and style for shrimple subs

* WIP(frontend): draft of main subs page view

* fix hourglass count

* Fix hourglass logic for upgrades

* fix admin panel display

* WIP(subs): extant Stripe state

* fix admin panel strings

* fix missing transaction type

* add new field for cumulative subscription count

* show date for hourglass bonus if it was received

* fix test

* feat(subscription): max Gems progress readout

* fix(css): correct and refactor heights and selection states

* fix(subs): correct border-radius and redirect

* fix(stripe): correct redirect after success

* Admin panel display fixes

* don’t give additional HG for new sub if they already got one this month

* fix issue with promo hourglasses

* fix(subscription): update layout when gifting

* fix(subscriptions): more gift layout revisions

* fix(subscriptions): minor visual updates

* fix(subs): pass autoRenews through Stripe

* fix(subs): gifts DON't renew

* fix(lint): unnecessary ternary

* fix(lint): do negate object ig

* fix(subs): try again on gifts

* fix(subs): unhovery and un-12-monthy

* fix bug with incorrectly giving HG bonus

* remove only

* fix test

* fix test

* fix(subs): also redirect to subs after gift sub

* fix(subs): fix typeError

* fix(g1g1): don't try to find Gems promo during bogo

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <sabe@habitica.com>

* chore(sprites): update subproject

* fix(layout): tighten cancellation note

* fix(subs): Google wording and HG escape

* chore(testing): fake g1g1 dates

* fix(subs): don't hide HG preview entirely

* fix(subs): center next hourglass message

* working validatedTextInput.vue within start-new-conversation-input-header.vue 🎉

* fix(git): remove changes from old develop

* Revert "fix(git): remove changes from old develop"

This reverts commit 0e30f7df00.

* fix(git): no actually just this file i guesss

* adding an empty loading state, hiding

* fought the avatar arch nemesis again

* fix chatMessages (party chat) message spacing

* move disabled text back to above the input area - re-enable input area

* show disabled private messages top panel

* fix font color

* fixing uiStates - removing disabled - moving the own user check to the last

* fix(lint): add missing prop defaults

* fix(lint): object default should be fn

* fix(chat): correct grammar in error

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2025-01-16 16:52:24 -06:00
757 changed files with 17079 additions and 26472 deletions
+9 -9
View File
@@ -1,12 +1,12 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": true
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": true
}
}
}
]
]
]
}
}
-9
View File
@@ -7,14 +7,5 @@ module.exports = {
rules: {
'prefer-regex-literals': 'warn',
'import/no-extraneous-dependencies': 'off',
'require-await': 'error',
},
overrides: [
{
files: ['migrations/**', 'gulp/**'], // Or *.test.js
rules: {
'require-await': 'off',
},
},
],
};
+1 -8
View File
@@ -1,13 +1,6 @@
name: Test
on:
push:
branches-ignore:
- 'phillip/**'
- 'sabrecat/**'
- 'kalista/**'
- 'natalie/**'
pull_request:
on: [push, pull_request]
permissions:
contents: read
+1 -1
View File
@@ -2,4 +2,4 @@
This webpage includes the documentation for version 3 of the [Habitica](https://habitica.com) API.
If you're developing a 3rd party tool that uses the Habitica API, read the [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines), which describe how to be a responsible user of our server resources!
If you're developing a 3rd party tool that uses the Habitica API you should read the [Guidance for Comrades](https://habitica.fandom.com/wiki/Guidance_for_Comrades) and in particular the section called [Rules for Third-Party Tools](https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools) which includes suggestions on how to best use the API and the rules to follow when interacting with it.
+1 -2
View File
@@ -93,6 +93,5 @@
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
"SLOW_REQUEST_THRESHOLD": 1000
"CONTENT_SWITCHOVER_TIME_OFFSET": 8
}
-9
View File
@@ -64,15 +64,6 @@ function filterFile (file) {
if (file.relative.indexOf('icon_background') === 0) {
return false;
}
if (file.relative.indexOf('notif_') === 0) {
return false;
}
if (file.relative.indexOf('quest_') === 0) {
return false;
}
if (file.relative.indexOf('inventory_quest_') === 0) {
return false;
}
return true;
}
-11
View File
@@ -1,11 +0,0 @@
import gulp from 'gulp';
import nodemon from 'gulp-nodemon';
import pkg from '../package.json';
gulp.task('nodemon', done => {
nodemon({
script: pkg.main,
});
done();
});
-6
View File
@@ -49,12 +49,6 @@ function integrationTestCommand (testDir) {
}
/* Test task definitions */
gulp.task('test:nodemon', gulp.series(done => {
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
done();
}, 'nodemon'));
gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
-1
View File
@@ -21,7 +21,6 @@ if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-e
require('./gulp/gulp-build'); // eslint-disable-line global-require
require('./gulp/gulp-console'); // eslint-disable-line global-require
require('./gulp/gulp-sprites'); // eslint-disable-line global-require
require('./gulp/gulp-start'); // eslint-disable-line global-require
require('./gulp/gulp-tests'); // eslint-disable-line global-require
require('./gulp/gulp-transifex-test'); // eslint-disable-line global-require
require('gulp').task('default', gulp.series('test')); // eslint-disable-line global-require
+1 -1
View File
@@ -37,7 +37,7 @@ let consoleStamp = require('console-stamp');
consoleStamp(console);
// Initialize configuration
require('../../website/server/libs/api-v3/setupNconf')();
require('../../website/server/libs/api-v3/setupNconf').default();
let MONGODB_OLD = nconf.get('MONGODB_OLD');
let MONGODB_NEW = nconf.get('MONGODB_NEW');
+1 -1
View File
@@ -32,7 +32,7 @@ let moment = require('moment');
consoleStamp(console);
// Initialize configuration
require('../../website/server/libs/api-v3/setupNconf')();
require('../../website/server/libs/api-v3/setupNconf').default();
let MONGODB_OLD = nconf.get('MONGODB_OLD');
let MONGODB_NEW = nconf.get('MONGODB_NEW');
+1 -1
View File
@@ -6,7 +6,7 @@ require('@babel/register'); // eslint-disable-line import/no-extraneous-dependen
function setUpServer () {
const nconf = require('nconf'); // eslint-disable-line global-require, no-unused-vars
const mongoose = require('mongoose'); // eslint-disable-line global-require, no-unused-vars
const setupNconf = require('../website/server/libs/setupNconf'); // eslint-disable-line global-require
const setupNconf = require('../website/server/libs/setupNconf').default; // eslint-disable-line global-require
setupNconf();
+251 -327
View File
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.36.5",
"version": "5.32.5",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -38,7 +38,6 @@
"gulp-babel": "^8.0.0",
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
@@ -50,12 +49,12 @@
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^8.9.5",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^7.8.3",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"nodemon": "^2.0.20",
"nodemon": "^3.1.9",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
@@ -100,23 +99,22 @@
"test:sanity": "nyc --silent --no-clean mocha test/sanity --recursive",
"test:common": "nyc --silent --no-clean mocha test/common --recursive",
"test:content": "nyc --silent --no-clean mocha test/content --recursive",
"test:nodemon": "gulp test:nodemon",
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:build": "cd website/client && npm run build",
"client:unit": "cd website/client && npm run test:unit",
"start": "gulp nodemon",
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 7.0.21 -l ubuntu2404 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 7.0.21 -l ubuntu2404 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"debug": "node --watch --inspect ./website/server/index.js",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
},
"devDependencies": {
"axios": "^1.8.2",
"axios": "^1.7.4",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
+3 -2
View File
@@ -71,14 +71,15 @@ async function deleteHabiticaData (user, email) {
}
async function processEmailAddress (email) {
const emailRegex = new RegExp(`^${email}$`, 'i');
const localUsers = await User.find(
{ 'auth.local.email': email },
{ 'auth.local.email': emailRegex },
{ _id: 1, apiToken: 1, auth: 1 },
).exec();
const socialUsers = await User.find(
{
'auth.local.email': { $ne: email },
'auth.local.email': { $not: emailRegex },
$or: [
{ 'auth.facebook.emails.value': email },
{ 'auth.google.emails.value': email },
+119 -250
View File
@@ -2,22 +2,13 @@
import moment from 'moment';
import nconf from 'nconf';
import requireAgain from 'require-again';
import { v4 as generateUUID } from 'uuid';
import {
generateRes,
generateReq,
generateTodo,
generateDaily,
} from '../../../helpers/api-unit.helper';
import { cron, cronWrapper } from '../../../../website/server/libs/cron';
import { recoverCron, cron } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common';
import * as analytics from '../../../../website/server/libs/analyticsService';
import { model as Group } from '../../../../website/server/models/group';
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
// const scoreTask = common.ops.scoreTask;
const pathToCronLib = '../../../../website/server/libs/cron';
@@ -1209,7 +1200,7 @@ describe('cron', async () => {
it('increments perfect day achievement if all (at least 1) due dailies were completed', async () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].isDue = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
@@ -1221,7 +1212,7 @@ describe('cron', async () => {
it('does not increment perfect day achievement if no due dailies', async () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].isDue = false;
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
@@ -1233,7 +1224,7 @@ describe('cron', async () => {
it('gives perfect day buff if all (at least 1) due dailies were completed', async () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].isDue = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const previousBuffs = user.stats.buffs.toObject();
@@ -1251,7 +1242,7 @@ describe('cron', async () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].isDue = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const previousBuffs = user.stats.buffs.toObject();
@@ -1268,7 +1259,7 @@ describe('cron', async () => {
it('clears buffs if user does not have a perfect day (no due dailys)', async () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].isDue = false;
tasksByType.dailys[0].startDate = moment(new Date()).add({ days: 1 });
user.stats.buffs = {
str: 1,
@@ -1497,6 +1488,78 @@ describe('cron', async () => {
});
});
describe('notifications', async () => {
it('adds a user notification', async () => {
const mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
daysMissed = 1;
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.notifications.length).to.be.greaterThan(0);
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore,
mp: user.stats.mp - mpBefore,
});
common.statsComputed.restore();
});
it('condenses multiple notifications into one', async () => {
const mpBefore1 = user.stats.mp;
tasksByType.dailys[0].completed = true;
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
daysMissed = 1;
const hpBefore1 = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.notifications.length).to.be.greaterThan(0);
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore1,
mp: user.stats.mp - mpBefore1,
});
const notifsBefore2 = user.notifications.length;
const hpBefore2 = user.stats.hp;
const mpBefore2 = user.stats.mp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.notifications.length - notifsBefore2).to.equal(0);
expect(user.notifications[0].type).to.not.equal('CRON');
expect(user.notifications[1].type).to.equal('CRON');
expect(user.notifications[1].data).to.eql({
hp: user.stats.hp - hpBefore2 - (hpBefore2 - hpBefore1),
mp: user.stats.mp - mpBefore2 - (mpBefore2 - mpBefore1),
});
expect(user.notifications[0].type).to.not.equal('CRON');
common.statsComputed.restore();
});
});
describe('private messages', async () => {
let lastMessageId;
@@ -1543,7 +1606,7 @@ describe('cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.notifications.length).to.eql(1);
expect(user.notifications.length).to.be.greaterThan(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
});
@@ -1757,258 +1820,64 @@ describe('cron', async () => {
});
});
describe('cron wrapper', () => {
let res; let
req;
let user;
describe('recoverCron', async () => {
let locals; let status; let
execStub;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analytics;
execStub = sandbox.stub();
sandbox.stub(User, 'findOne').returns({ exec: execStub });
status = { times: 0 };
locals = {
user: new User({
auth: {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
}),
};
});
afterEach(() => {
afterEach(async () => {
sandbox.restore();
});
it('calls next when user is not attached', async () => {
res.locals.user = null;
await cronWrapper(req, res);
});
it('calls next when days have not been missed', async () => {
await cronWrapper(req, res);
});
it('should clear todos older than 30 days for free users', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await task.save();
await user.save();
await cronWrapper(req, res);
const taskRes = await Tasks.Task.findOne({ _id: task._id });
expect(taskRes).to.not.exist;
});
it('should not clear todos older than 30 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await Promise.all([task.save(), user.save()]);
await cronWrapper(req, res);
const taskRes = await Tasks.Task.findOne({ _id: task._id });
expect(taskRes).to.exist;
});
it('should clear todos older than 90 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
task.completed = true;
await task.save();
await user.save();
await cronWrapper(req, res);
const taskRes = await Tasks.Task.findOne({ _id: task._id });
expect(taskRes).to.not.exist;
});
it('should call next if user was not modified after cron', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
await cronWrapper(req, res);
expect(hpBefore).to.equal(user.stats.hp);
});
it('runs cron if previous cron was incomplete', async () => {
user.lastCron = moment(new Date()).subtract({ days: 1 });
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
const now = new Date();
await user.save();
await cronWrapper(req, res);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await user.save();
await cronWrapper(req, res);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
});
it('does damage for missing dailies', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
await user.save();
await cronWrapper(req, res);
const updatedUser = await User.findOne({ _id: user._id });
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
});
it('updates tasks', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const todo = generateTodo(user);
const todoValueBefore = todo.value;
await Promise.all([todo.save(), user.save()]);
await cronWrapper(req, res);
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
expect(todoFound.value).to.be.lessThan(todoValueBefore);
});
it('updates large number of tasks', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const todo = generateTodo(user);
const todoValueBefore = todo.value;
const start = new Date();
const saves = [todo.save(), user.save()];
for (let i = 0; i < 200; i += 1) {
const newTodo = generateTodo(user);
newTodo.value = i;
saves.push(newTodo.save());
}
await Promise.all(saves);
await cronWrapper(req, res);
const duration = new Date() - start;
expect(duration).to.be.lessThan(1000);
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
expect(moment(start).isSame(user.lastCron, 'day'));
expect(moment(start).isSame(user.auth.timestamps.loggedin, 'day'));
expect(todoFound.value).to.be.lessThan(todoValueBefore);
});
it('fails entire cron if one task is failing', async () => {
const lastCron = moment(new Date()).subtract({ days: 2 });
user.lastCron = lastCron;
const todo = generateTodo(user);
const todoValueBefore = todo.value;
const badTodo = generateTodo(user);
badTodo.text = 'bad todo';
badTodo.attribute = 'bad';
await Promise.all([badTodo.save({ validateBeforeSave: false }), todo.save(), user.save()]);
it('throws an error if user cannot be found', async () => {
execStub.returns(Promise.resolve(null));
try {
await cronWrapper(req, res);
await recoverCron(status, locals);
throw new Error('no exception when user cannot be found');
} catch (err) {
expect(err).to.exist;
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
}
const todoFound = await Tasks.Task.findOne({ _id: todo._id });
expect(moment(lastCron).isSame(user.lastCron, 'day'));
expect(todoFound.value).to.be.equal(todoValueBefore);
});
it('applies quest progress', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
it('increases status.times count and reruns up to 4 times', async () => {
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
execStub.onCall(4).returns(Promise.resolve({ _cronSignature: 'NOT_RUNNING' }));
const questKey = 'dilatory';
user.party.quest.key = questKey;
await recoverCron(status, locals);
const party = new Group({
type: 'party',
name: generateUUID(),
leader: user._id,
});
party.quest.members[user._id] = true;
party.quest.key = questKey;
await party.save();
user.party._id = party._id;
await user.save();
party.startQuest(user);
await cronWrapper(req, res);
const updatedUser = await User.findOne({ _id: user._id });
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
expect(status.times).to.eql(4);
expect(locals.user).to.eql({ _cronSignature: 'NOT_RUNNING' });
});
it('cronSignature less than 5 minutes ago should error', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
it('throws an error if recoverCron runs 5 times', async () => {
execStub.returns(Promise.resolve({ _cronSignature: 'RUNNING_CRON' }));
try {
await cronWrapper(req, res);
await recoverCron(status, locals);
throw new Error('no exception when recoverCron runs 5 times');
} catch (err) {
expect(err).to.exist;
expect(status.times).to.eql(5);
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
}
});
it('cronSignature longer than an hour ago should allow cron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
await cronWrapper(req, res);
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
});
it('cron should not run more than once', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
const result = await Promise.allSettled([
cronWrapper(req, res),
cronWrapper(req, res),
new Promise((resolve, reject) => {
setTimeout(async () => {
try {
const runResult = await cronWrapper(req, res);
if (runResult !== null) {
reject(new Error('cron ran more than once'));
} else {
resolve();
}
} catch (err) {
reject(err);
}
}, 200);
}),
]);
expect(result.filter(r => r.status === 'fulfilled')).to.have.lengthOf(2);
expect(result.filter(r => r.status === 'rejected')).to.have.lengthOf(1);
});
});
+8 -10
View File
@@ -171,23 +171,23 @@ describe('emails', () => {
expect(got.post).not.to.be.called;
});
it('throws error when mail target is only a string', async () => {
it('throws error when mail target is only a string', () => {
const emailType = 'an email type';
const mailingInfo = 'my email';
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when mail target has no _id or email', async () => {
it('throws error when mail target has no _id or email', () => {
const emailType = 'an email type';
const mailingInfo = {
};
await expect(sendTxn(mailingInfo, emailType)).to.be.rejectedWith('Argument Error mailingInfoArray: does not contain email or _id');
expect(sendTxn(mailingInfo, emailType)).to.throw;
});
it('throws error when variables not an array', async () => {
it('throws error when variables not an array', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
@@ -195,10 +195,9 @@ describe('emails', () => {
};
const variables = {};
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: is not an array');
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws error when variables array not contain name/content', async () => {
it('throws error when variables array not contain name/content', () => {
const emailType = 'an email type';
const mailingInfo = {
name: 'my name',
@@ -210,9 +209,8 @@ describe('emails', () => {
},
];
await expect(sendTxn(mailingInfo, emailType, variables)).to.be.rejectedWith('Argument Error variables: does not contain name or content');
expect(sendTxn(mailingInfo, emailType, variables)).to.throw;
});
it('throws no error when variables array contain name but no content', () => {
const emailType = 'an email type';
const mailingInfo = {
+19
View File
@@ -1,4 +1,5 @@
import os from 'os';
import nconf from 'nconf';
import requireAgain from 'require-again';
const pathToMongoLib = '../../../../website/server/libs/mongodb';
@@ -28,4 +29,22 @@ describe('mongodb', () => {
expect(string).to.equal('mongodb://hostname:3030');
});
});
describe('getDefaultConnectionOptions', () => {
it('returns development config when IS_PROD is false', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
});
it('returns production config when IS_PROD is true', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const mongoLibOverride = requireAgain(pathToMongoLib);
const options = mongoLibOverride.getDefaultConnectionOptions();
expect(options).to.have.all.keys(['useNewUrlParser', 'useUnifiedTopology']);
});
});
});
+332
View File
@@ -0,0 +1,332 @@
import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import {
generateRes,
generateReq,
generateTodo,
generateDaily,
} from '../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../website/server/middlewares/cron';
import { model as User } from '../../../../website/server/models/user';
import { model as Group } from '../../../../website/server/models/group';
import * as Tasks from '../../../../website/server/models/task';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
import * as cronLib from '../../../../website/server/libs/cron';
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
describe('cron middleware', () => {
let res; let
req;
let user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analyticsService;
});
afterEach(() => {
sandbox.restore();
});
it('calls next when user is not attached', done => {
res.locals.user = null;
cronMiddleware(req, res, done);
});
it('calls next when days have not been missed', done => {
cronMiddleware(req, res, done);
});
it('should clear todos older than 30 days for free users', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
resolve();
});
return null;
});
});
});
it('should not clear todos older than 30 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 31 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.exist;
return resolve();
});
return null;
});
});
});
it('should clear todos older than 90 days for subscribed users', async () => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({ days: 2 });
const task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({ days: 91 });
task.completed = true;
await task.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
Tasks.Task.findOne({ _id: task }).then(foundTask => {
expect(foundTask).to.not.exist;
return resolve();
});
return null;
});
});
});
it('should call next if user was not modified after cron', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(hpBefore).to.equal(user.stats.hp);
return resolve();
});
});
});
it('runs cron if previous cron was incomplete', async () => {
user.lastCron = moment(new Date()).subtract({ days: 1 });
user.auth.timestamps.loggedin = moment(new Date()).subtract({ days: 4 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('updates user.auth.timestamps.loggedin and lastCron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.lastCron, 'day'));
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
return resolve();
});
});
});
it('does damage for missing dailies', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
});
});
});
it('updates tasks', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const todo = generateTodo(user);
const todoValueBefore = todo.value;
await Promise.all([todo.save(), user.save()]);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return Tasks.Task.findOne({ _id: todo._id }).then(todoFound => {
expect(todoFound.value).to.be.lessThan(todoValueBefore);
return resolve();
});
});
});
});
it('applies quest progress', async () => {
const hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({ days: 2 });
const daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({ days: 2 });
await daily.save();
const questKey = 'dilatory';
user.party.quest.key = questKey;
const party = new Group({
type: 'party',
name: generateUUID(),
leader: user._id,
});
party.quest.members[user._id] = true;
party.quest.key = questKey;
await party.save();
user.party._id = party._id;
await user.save();
party.startQuest(user);
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return User.findOne({ _id: user._id }).then(updatedUser => {
expect(updatedUser.stats.hp).to.be.lessThan(hpBefore);
return resolve();
});
});
});
});
it('recovers from failed cron and does not error when user is already cronning', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
const updatedUser = user.toObject();
updatedUser.matchedCount = 0;
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'updateOne')
.withArgs({
_id: user._id,
$or: [
{ _cronSignature: 'NOT_RUNNING' },
{ _cronSignature: { $lt: sinon.match.number } },
],
})
.returns({
exec () {
return Promise.resolve(updatedUser);
},
});
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(cronLib.recoverCron).to.be.calledOnce;
return resolve();
});
});
});
it('cronSignature less than an hour ago should error', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
const expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (!err) return reject(new Error('Cron should have failed.'));
expect(err.message).to.be.equal(expectedErrMessage);
return resolve();
});
});
});
it('cronSignature longer than an hour ago should allow cron', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
const now = new Date();
await User.updateOne({
_id: user._id,
}, {
$set: {
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
},
}).exec();
await user.save();
await new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
return resolve();
});
});
});
it('cron should not run more than once', async () => {
user.lastCron = moment(new Date()).subtract({ days: 2 });
await user.save();
sandbox.spy(cronLib, 'cron');
await Promise.all([new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}), new Promise((resolve, reject) => {
setTimeout(() => {
cronMiddleware(req, res, err => {
if (err) return reject(err);
return resolve();
});
}, 400);
}),
]);
expect(cronLib.cron).to.be.calledOnce;
});
});
@@ -10,7 +10,6 @@ describe('GET /heroes/:heroId', () => {
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
'stats',
];
before(async () => {
@@ -11,7 +11,6 @@ describe('PUT /heroes/:heroId', () => {
const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
'stats',
];
before(async () => {
@@ -61,12 +60,12 @@ describe('PUT /heroes/:heroId', () => {
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
expect(heroRes.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
expect(heroRes.contributor.level).to.equal(1);
expect(heroRes.purchased.ads).to.equal(true);
// test hero values
await hero.sync();
expect(hero.balance).to.equal(3 + 2.5); // 3+2.5 for first contrib level
expect(hero.balance).to.equal(3 + 0.75); // 3+0.75 for first contrib level
expect(hero.contributor.level).to.equal(1);
expect(hero.purchased.ads).to.equal(true);
expect(hero.auth.blocked).to.equal(prevBlockState);
@@ -137,12 +136,12 @@ describe('PUT /heroes/:heroId', () => {
expect(heroRes.profile).to.have.all.keys(['name']);
// test response values
expect(heroRes.balance).to.equal(15); // 0+15 for sixth contrib level
expect(heroRes.balance).to.equal(1); // 0+1 for sixth contrib level
expect(heroRes.contributor.level).to.equal(6);
expect(heroRes.items.pets['Dragon-Hydra']).to.equal(5);
// test hero values
await hero.sync();
expect(hero.balance).to.equal(15); // 0+15 for sixth contrib level
expect(hero.balance).to.equal(1); // 0+1 for sixth contrib level
expect(hero.contributor.level).to.equal(6);
expect(hero.items.pets['Dragon-Hydra']).to.equal(5);
});
@@ -19,7 +19,7 @@ describe('GET /members/username/:username', () => {
});
});
it('returns a member\'s public data only', async () => {
it('returns a member public data only', async () => {
// make sure user has all the fields that can be returned by the getMember call
const member = await generateUser({
contributor: { level: 1 },
@@ -101,6 +101,34 @@ describe('GET /tasks/user', () => {
expect(allCompletedTodos[allCompletedTodos.length - 1].text).to.equal('todo to complete 2');
});
it('returns only some completed todos if req.query.type is "completedTodos" or "_allCompletedTodos"', async () => {
const LIMIT = 30;
const numberOfTodos = LIMIT + 1;
const todosInput = [];
for (let i = 0; i < numberOfTodos; i += 1) {
todosInput[i] = { text: `todo to complete ${i}`, type: 'todo' };
}
const todos = await user.post('/tasks/user', todosInput);
await user.sync();
const initialTodoCount = user.tasksOrder.todos.length;
for (let i = 0; i < numberOfTodos; i += 1) {
const id = todos[i]._id;
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
}
await user.sync();
expect(user.tasksOrder.todos.length).to.equal(initialTodoCount - numberOfTodos);
const completedTodos = await user.get('/tasks/user?type=completedTodos');
expect(completedTodos.length).to.equal(LIMIT);
const allCompletedTodos = await user.get('/tasks/user?type=_allCompletedTodos');
expect(allCompletedTodos.length).to.equal(numberOfTodos);
});
it('returns dailies with isDue for the date specified', async () => {
// @TODO Add required format
const startDate = moment().subtract('1', 'days').toISOString();
@@ -26,7 +26,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
userToSendMessage = await generateUser();
});
it('returns an error when private message is not found', async () => {
it('Returns an error when private message is not found', async () => {
await expect(userToSendMessage.post(getLikeUrl('some-unknown-id')))
.to.eventually.be.rejected.and.eql({
code: 404,
@@ -35,7 +35,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
});
});
it('likes a message', async () => {
it('Likes a message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
@@ -57,7 +57,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, receiver._id, true);
});
it('allows a user to like their own private message', async () => {
it('Allows to likes their own private message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
@@ -78,7 +78,7 @@ describe('POST /inbox/like-private-message/:messageId', () => {
expectMessagesLikeStatus(receiversMessages, uniqueMessageId, userToSendMessage._id, true);
});
it('unlikes a message', async () => {
it('Unlikes a message', async () => {
const receiver = await generateUser();
const sentMessageResult = await userToSendMessage.post('/members/send-private-message', {
+1 -1
View File
@@ -10,7 +10,7 @@ describe('events', () => {
});
it('returns empty array when no events are active', () => {
clock = sinon.useFakeTimers(new Date('2024-01-11'));
clock = sinon.useFakeTimers(new Date('2024-01-08'));
const events = getRepeatingEvents();
expect(events).to.be.empty;
});
+1 -1
View File
@@ -190,7 +190,7 @@ describe('Content Schedule', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(6);
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
+1 -1
View File
@@ -19,6 +19,6 @@ const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(global.sinon);
global.sandbox = sinon.createSandbox();
const setupNconf = require('../../website/server/libs/setupNconf');
const setupNconf = require('../../website/server/libs/setupNconf').default;
setupNconf('./config.json.example');
+9 -4
View File
@@ -74,10 +74,15 @@ export async function getDocument (collectionName, doc) {
}
before(done => {
mongoose.connection.once('open', async err => {
if (err) throw err;
await resetHabiticaDB();
done();
mongoose.connection.on('open', err => {
if (err) return done(err);
return resetHabiticaDB()
.then(() => {
done();
})
.catch(error => {
throw error;
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
const nconf = require('nconf');
const mongoose = require('mongoose');
const setupNconf = require('../../website/server/libs/setupNconf');
const setupNconf = require('../../website/server/libs/setupNconf').default;
// fix further imports of require/import syntaxes
require('@babel/register');
+1 -3
View File
@@ -3,6 +3,7 @@ module.exports = {
root: true,
env: {
node: true,
es2021: true,
},
extends: [
'habitrpg/lib/vue',
@@ -39,7 +40,4 @@ module.exports = {
order: ['template', 'style', 'script'],
}],
},
parserOptions: {
parser: 'babel-eslint',
},
};
-9
View File
@@ -1,9 +0,0 @@
/* eslint-disable import/no-commonjs */
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
};
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica - Gamify Your Life</title>
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
<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">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main-static.js"></script>
</head>
<body>
<div id="loading-screen">
<svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
d="M79.05 72.15c-.8-1.766-2.643-2.62-3.845-1.766-1.201.855-2.867.985-4.448.602-1.584-.385-1.885-4.01-1.543-8.195.342-4.184.909-5.795 1.267-7.314.404-1.524 2.191-1.404 2.405-.209.215 1.196 1.454 1.196 3.266-.979 1.811-2.175 1.543-8.52-.546-13.684-2.088-5.163.817-4.661 1.66-4.149.844.513 1.362-.255 1.156-3.2-.204-2.945-2.916-5.247-5.096-6.657-2.184-1.41-4.842-2.967-4.78-6.745.063-3.777 5.2-3.658 5.897-3.596.697.063 2.037-.233 1.264-4.157-.773-3.924-3.575-4.673-5.332-4.567-1.758.106-2.943 1.071-5.427.133-2.484-.938-4.136-.572-6.45-.057-2.313.515-5.343 1.94-9.112 2.959-1.989.545-2.661.683-4.828.718-1.33.02-1.885 1.633-.106 3.61 1.408 1.608 4.597 2.036 6.515 1.768 1.236-.174 1.521.645 1.407 1.85a20.023 20.023 0 0 0-.024 4.488c.198 1.5.45 4.051-.258 5.713-.35.817-1.361 1.693-2.449 1.633-1.413-.084-2.555-1.75-3.537-3.726-2.06-4.152-4.48-5.033-13.509-8.835-8.12-3.417-12.516-8.749-15.24-12.185-2.421-3.042-4.846-1.89-4.626.855.179 2.128 1.48 9.008 4.781 13.141 4.058 6.314 10.32 9.177 17.534 9.739 1.885.149 3.065.52 3.225 1.383.236 1.835-1.557 3.11-4.898 2.722-3.341-.39-4.768.22-4.103 2.121 2.123 4.477 7.021 4.672 9.058 4.857.686.122 3.114 0 4.41.355 1.51.418 1.836 2.514-.353 3.648-3.892 1.903-5.59 3.479-7.561 7.075-1.486 2.826-2.77 7.555-1.435 14.365 1.283 6.62-8.342 6.83-12.497 5.89-1.793-.377-3.675-3.778.716-6.625 3.553-2.305 4.269-3.724 4.111-6.642-.184-3.4-2.058-3.644-2.053-6.598v-7.05c0-.602-.488-1.088-1.087-1.088h-3.334a1.087 1.087 0 0 1-1.087-1.087v-4.25c0-.602-.488-1.087-1.088-1.087h-3.317a1.087 1.087 0 0 1-1.087-1.088v-3.81c0-.602-.489-1.087-1.088-1.087h-4.04a1.087 1.087 0 0 1-1.089-1.088V26.25c0-.602-.488-1.088-1.087-1.088H1.088C.485 25.161 0 25.65 0 26.25v4.26c0 .602.488 1.087 1.088 1.087h4.049c.602 0 1.087.489 1.087 1.088v15.192c0 .602.489 1.087 1.088 1.087h4.277c.602 0 1.088.489 1.088 1.088v4.968c0 .602.488 1.087 1.087 1.087h6.005c1.836-.13 2.156 2.335 2.137 3.214-.04 2.007-2.308 2.652-3.382 3.487-2.861 2.21-5.077 4.459-3.78 8.781l.032.09c2.362 5.017 8.855 4.499 12.956 4.499h25.817c1.459 0 2.959.339 2.614-1.362-.342-1.7-1.063-4.024-3.162-4.024-2.1 0-1.758 1.166-3.81.57-2.054-.597-2.057-3.371 1.027-8.198 3.19-4.122 8.652-3.81 11.952-.895 3.301 2.915 2.325 7.978 1.633 10.885-.396 2.048.545 3.06 1.67 3.032H78.58c2.015-.035 1.62-1.391.464-4.035h.008z"
fill="#fff"
>
</path>
</svg>
</div>
<div id="app"></div>
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
</body>
</html>
@@ -12,6 +12,7 @@
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="loading-screen">
@@ -28,10 +29,9 @@
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
<script type='text/javascript' src='/api/v4/i18n/browser-script'></script>
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
import fs from 'fs';
import path from 'path';
import { approvedLanguages } from '../common/script/libs/i18n';
const localePath = path.join(__dirname, '../common/locales/');
const translations = {};
const momentLangs = {};
function loadFile (file) {
const contents = fs.readFileSync(file);
return JSON.parse(contents);
}
function _loadTranslations (locale) {
const files = fs.readdirSync(path.join(localePath, locale));
translations[locale] = {};
files.forEach(file => {
if (path.extname(file) !== '.json') return;
const t = loadFile(path.join(localePath, locale, file))
translations[locale] = {
...translations[locale],
...t,
}
});
}
// First fetch English strings so we can merge them with missing strings in other languages
_loadTranslations('en');
// Then load all other languages
approvedLanguages.forEach(file => {
if (file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
_loadTranslations(file);
// Strip empty strings, then merge missing strings from english
translations[file] = {
...translations.en,
...translations[file],
}
});
export default function localePlugin() {
const virtualModuleId = 'virtual:translations'
const resolvedVirtualModuleId = '\0' + virtualModuleId
return {
name: 'locale', // required, will show up in warnings and errors
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `export default ${JSON.stringify(translations)}`;
}
},
}
}
+6319 -8000
View File
File diff suppressed because it is too large Load Diff
+18 -17
View File
@@ -3,28 +3,25 @@
"version": "1.0.0",
"private": true,
"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 .",
"serve": "vite",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run",
"test:unit:watch": "vitest watch",
"lint": "eslint --ext .js,.vue --ignore-path ../../.gitignore --fix .",
"lint-no-fix": "eslint --ext .js,.vue --no-fix src",
"postinstall": "node ./scripts/npm-postinstall.js"
},
"dependencies": {
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-router": "^5.0.8",
"@vue/cli-plugin-unit-mocha": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vitejs/plugin-vue2": "^2.3.3",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.21.3",
"assert": "^2.1.0",
"autoprefixer": "^10.4.20",
"axios": "^0.28.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"core-js": "^3.33.1",
"eslint": "7.32.0",
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
@@ -35,30 +32,34 @@
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"moment-locales-webpack-plugin": "^1.2.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^14.1.1",
"sinon": "^17.0.1",
"stopword": "^2.0.8",
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite-plugin-prerender": "^1.0.8",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vue-template-babel-compiler": "^2.0.0",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
"@prerenderer/renderer-puppeteer": "^1.2.4",
"@prerenderer/rollup-plugin": "^0.3.12",
"@vitest/browser": "^3.0.5",
"babel-plugin-lodash": "^3.3.4",
"chai": "^5.1.0",
"inspectpack": "^4.7.1",
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
}
}
+3 -3
View File
@@ -34,7 +34,7 @@
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#loading-screen-inapp {
#melior {
@@ -90,7 +90,7 @@
</style>
<style lang='scss'>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.modal-backdrop {
opacity: .9 !important;
@@ -111,7 +111,7 @@ import { mapState } from '@/libs/store';
import userMain from '@/pages/user-main';
import snackbars from '@/components/snackbars/notifications';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
name: 'App',
+1 -6
View File
@@ -22,8 +22,7 @@
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup, .Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi {
width: 68px;
height: 68px;
}
@@ -48,10 +47,6 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
}
.Pet_HatchingPotion_Cryptid {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
}
.Gems {
display:inline-block;
margin-right:5px;
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -19,7 +19,7 @@
top: -16px !important;
}
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi, Cryptid;
$foolPets: Veggie, Dessert, VirtualPet, TeaShop, Fungi;
@each $foolPet in $foolPets {
.Pet.Pet-FlyingPig-#{$foolPet} {
+1 -1
View File
@@ -1,5 +1,5 @@
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.featured-label {
width: auto;
+2 -2
View File
@@ -2,7 +2,7 @@
$grid-gutter-width: 24px;
// Bootstrap and its default variables
@import 'node_modules/bootstrap/scss/bootstrap';
@import '~/bootstrap/scss/bootstrap';
// Bootstrap Vue styles
@import 'node_modules/bootstrap-vue/dist/bootstrap-vue';
@import '~/bootstrap-vue/dist/bootstrap-vue';
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h1 {
margin-top: 0px;
+3 -3
View File
@@ -61,13 +61,13 @@ input, textarea, input.form-control, textarea.form-control {
&.input-valid {
padding-right: 27px;
background-image: url(~@/assets/svg/for-css/check.svg);
background-image: url(@/assets/svg/for-css/check.svg);
background-size: 1rem;
}
&.input-invalid {
padding-right: 40px;
background-image: url(~@/assets/svg/for-css/alert.svg);
background-image: url(@/assets/svg/for-css/alert.svg);
background-size: 16px 16px;
border-color: $red-100 !important;
}
@@ -239,7 +239,7 @@ $bg-disabled-control: $gray-10;
&:checked~.custom-control-label::after {
width: 18px;
height: 18px;
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
background-size: 13px 10px;
}
@@ -29,13 +29,13 @@
}
.iconalert-success::before {
background-image: url(~@/assets/svg/for-css/checkbox-white.svg);
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
background-size: 13px 10px;
background-color: #1ca372;
}
.iconalert-warning::before, .iconalert-error::before {
background-image: url(~@/assets/svg/for-css/alert-white.svg);
background-image: url(@/assets/svg/for-css/alert-white.svg);
background-size: 16px 16px;
}
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.modal {
z-index: 1350;
+1 -1
View File
@@ -1,4 +1,4 @@
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.container-fluid.static-view {
margin: 5em 2em 0 2em;
@@ -1,3 +0,0 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M4,0C1.79,0,0,1.79,0,4v16c0,2.21,1.79,4,4,4h16c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4H4ZM12,11.57c-.72-1.49-2.7-4.26-4.53-5.63-1.32-.99-3.47-1.75-3.47.68,0,.49.28,4.08.44,4.66.57,2.03,2.65,2.55,4.5,2.23-3.24.55-4.06,2.36-2.28,4.17,3.38,3.44,4.85-.86,5.23-1.97h0s0,0,0,0c.07-.2.1-.29.1-.21,0-.08.03.01.1.22h0c.38,1.1,1.85,5.41,5.23,1.97,1.78-1.81.95-3.63-2.28-4.17,1.85.31,3.93-.2,4.5-2.23.16-.58.44-4.18.44-4.66,0-2.43-2.14-1.67-3.47-.68-1.83,1.37-3.81,4.14-4.53,5.63Z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 572 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

+10 -4
View File
@@ -4,7 +4,7 @@
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
<img
:class="retiredChatPage ? 'mt-5' : 'image-404'"
src="~@/assets/images/404.png"
src="@/assets/images/404.png"
>
<div v-if="retiredChatPage">
<h1>
@@ -25,9 +25,9 @@
<router-link to="/">
Homepage
</router-link>or
<a href="mailto:admin@habitica.com">
<router-link :to="contactUsLink">
Contact Us
</a>about the issue.
</router-link>about the issue.
</p>
</div>
</div>
@@ -40,6 +40,12 @@ import { mapState } from '@/libs/store';
export default {
computed: {
...mapState(['isUserLoggedIn']),
contactUsLink () {
if (this.isUserLoggedIn) {
return { name: 'guild', params: { groupId: 'a29da26b-37de-4a71-b0c6-48e72a900dac' } };
}
return { name: 'contact' };
},
retiredChatPage () {
return this.$route.fullPath.indexOf('/groups') !== -1;
},
@@ -48,7 +54,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h1, .static-wrapper h1 {
color: $purple-200;
@@ -8,7 +8,7 @@
<div class="modal-body">
<div class="row">
<div class="col-6 offset-3">
<Sprite image-name="shop_armoire" />
<div class="shop_armoire"></div>
<p>{{ $t('armoireLastItem') }}</p>
<p>{{ $t('armoireNotesEmpty') }}</p>
</div>
@@ -34,12 +34,7 @@
</style>
<script>
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'armoire-empty');
@@ -95,11 +95,7 @@
@click="clickDisableClasses(); close();"
>{{ $t('optOutOfClasses') }}</span>
</div>
<div
v-once
class="opt-out-description"
v-html="$t('optOutOfClassesText')"
></div>
<span class="opt-out-description">{{ $t('optOutOfClassesText') }}</span>
</div>
</div>
</div>
@@ -107,7 +103,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.btn-primary:active {
border: 2px solid $purple-400 !important;
@@ -193,10 +189,10 @@
import Avatar from '../avatar';
import { mapState } from '@/libs/store';
import markdownDirective from '@/directives/markdown';
import warriorIcon from '@/assets/svg/warrior.svg';
import rogueIcon from '@/assets/svg/rogue.svg';
import healerIcon from '@/assets/svg/healer.svg';
import wizardIcon from '@/assets/svg/wizard.svg';
import warriorIcon from '@/assets/svg/warrior.svg?raw';
import rogueIcon from '@/assets/svg/rogue.svg?raw';
import healerIcon from '@/assets/svg/healer.svg?raw';
import wizardIcon from '@/assets/svg/wizard.svg?raw';
export default {
components: {
@@ -70,7 +70,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h2 {
color: $purple-200;
@@ -100,7 +100,7 @@
</style>
<script>
import closeIcon from '@/assets/svg/close.svg';
import closeIcon from '@/assets/svg/close.svg?raw';
import Sprite from '@/components/ui/sprite.vue';
export default {
@@ -45,7 +45,7 @@
</template>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
@import '@/assets/scss/mixins.scss';
#generic-achievement {
@include centeredModal();
@@ -61,7 +61,7 @@
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.content {
text-align: center;
@@ -98,7 +98,7 @@
<script>
import achievements from '@/../../common/script/content/achievements';
import { mapState } from '@/libs/store';
import svgClose from '@/assets/svg/close.svg';
import svgClose from '@/assets/svg/close.svg?raw';
import Sprite from '@/components/ui/sprite.vue';
export default {
@@ -48,7 +48,7 @@
></span>
</div>
<Sprite :image-name="questClass" />
<div :class="questClass"></div>
</section>
<!-- @TODO: Keep this? .checkboxinput(type='checkbox', v-model=
'user.preferences.suppressModals.levelUp', @change='changeLevelupSuppress()')
@@ -58,7 +58,7 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#level-up {
.modal-content {
@@ -150,15 +150,18 @@ label(style='display:inline-block') {{ $t('dontShowAgain') }}
section.greyed {
padding-bottom: 17px
}
.scroll {
margin: -11px auto 0;
}
}
</style>
<script>
import Avatar from '../avatar';
import Sprite from '@/components/ui/sprite';
import { mapState } from '@/libs/store';
import starGroup from '@/assets/svg/star-group.svg';
import sparkles from '@/assets/svg/sparkles-left.svg';
import starGroup from '@/assets/svg/star-group.svg?raw';
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
const levelQuests = {
15: 'atom1',
@@ -170,7 +173,6 @@ const levelQuests = {
export default {
components: {
Avatar,
Sprite,
},
data () {
return {
@@ -189,9 +191,7 @@ export default {
return this.user.stats.lvl in levelQuests;
},
questClass () {
const questKey = levelQuests[this.user.stats.lvl];
if (questKey) return `inventory_quest_scroll_${questKey}`;
return '';
return `scroll inventory_quest_scroll_${levelQuests[this.user.stats.lvl]}`;
},
},
methods: {
@@ -17,7 +17,7 @@
</h2>
<img
class="onboarding-complete-banner d-block"
src="~@/assets/images/onboarding-complete-banner@2x.png"
src="@/assets/images/onboarding-complete-banner@2x.png"
>
<p
v-once
@@ -59,7 +59,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h2 {
color: $purple-200;
@@ -100,7 +100,7 @@ button {
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
import svgClose from '@/assets/svg/close.svg?raw';
export default {
data () {
@@ -55,7 +55,7 @@
<p v-html="$t('moreGearAchievements')"></p>
<br>
</div>
<Sprite image-name="shop_armoire" />
<div class="shop_armoire"></div>
<p v-html="$t('armoireUnlocked')"></p>
<br>
<button
@@ -87,13 +87,11 @@
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import { mapState } from '@/libs/store';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
achievementFooter,
achievementAvatar,
Sprite,
},
computed: {
...mapState({ user: 'user.data' }),
@@ -73,7 +73,7 @@
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#won-challenge {
.modal-body {
@@ -96,7 +96,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.purple {
color: $purple-300;
@@ -146,9 +146,9 @@
<script>
import habiticaMarkdown from 'habitica-markdown';
import closeIcon from '@/components/shared/closeIcon';
import sparkles from '@/assets/svg/star-group.svg';
import gem from '@/assets/svg/gem.svg';
import stars from '@/assets/svg/sparkles-left.svg';
import sparkles from '@/assets/svg/star-group.svg?raw';
import gem from '@/assets/svg/gem.svg?raw';
import stars from '@/assets/svg/sparkles-left.svg?raw';
import { mapState } from '@/libs/store';
export default {
@@ -92,6 +92,8 @@ export default {
params: { userIdentifier },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
@@ -99,16 +101,14 @@ export default {
async loadUser (userIdentifier) {
const id = userIdentifier || this.user._id;
if (this.$router.currentRoute.name === 'adminPanelUser') {
await this.$router.push({
name: 'adminPanel',
});
}
await this.$router.push({
this.$router.push({
name: 'adminPanelUser',
params: { userIdentifier: id },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
@@ -1,15 +1,6 @@
import VueRouter from 'vue-router';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
methods: {
async saveHero ({
hero,
msg = 'User',
clearData,
reloadData,
}) {
async saveHero ({ hero, msg = 'User', clearData }) {
await this.$store.dispatch('hall:updateHero', { heroDetails: hero });
await this.$store.dispatch('snackbars:add', {
title: '',
@@ -23,20 +14,6 @@ export default {
// The admin should re-fetch the data if they need to keep working on that user.
this.$emit('clear-data');
this.$router.push({ name: 'adminPanel' });
} else if (reloadData) {
if (this.$router.currentRoute.name === 'adminPanelUser') {
await this.$router.push({
name: 'adminPanel',
});
}
await this.$router.push({
name: 'adminPanelUser',
params: { userIdentifier: hero._id },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
this.$router.go();
}
});
}
},
},
@@ -63,10 +63,6 @@ export default {
components: {
LoadingSpinner,
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
data () {
return {
userIdentifier: '',
@@ -78,6 +74,10 @@ export default {
computed: {
...mapState({ user: 'user.data' }),
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
watch: {
userIdentifier () {
this.isSearching = true;
@@ -17,7 +17,6 @@
<li
v-for="item in achievements"
:key="item.path"
v-b-tooltip.hover="item.notes"
>
<form @submit.prevent="saveItem(item)">
<span
@@ -28,7 +27,7 @@
{{ item.value }}
</span>
:
{{ item.text || item.key }} - <i> {{ item.key }} </i>
{{ item.text || item.key }}
</span>
<div
@@ -69,7 +68,6 @@
<li
v-for="item in nestedAchievements[achievementType]"
:key="item.path"
v-b-tooltip.hover="item.notes"
>
<form @submit.prevent="saveItem(item)">
<span
@@ -80,7 +78,7 @@
{{ item.value }}
</span>
:
{{ item.text || item.key }} - <i> {{ item.key }} </i>
{{ item.text || item.key }}
</span>
<div
@@ -145,28 +143,79 @@ function getText (achievementItem) {
}
const { titleKey } = achievementItem;
if (titleKey !== undefined) {
return i18n.t(titleKey);
return i18n.t(titleKey, 'en');
}
const { singularTitleKey } = achievementItem;
if (singularTitleKey !== undefined) {
return i18n.t(singularTitleKey);
return i18n.t(singularTitleKey, 'en');
}
return achievementItem.key;
}
function getNotes (achievementItem, count) {
if (achievementItem === undefined) {
return '';
function collateItemData (self) {
const achievements = [];
const nestedAchievements = {};
const basePath = 'achievements';
const ownedAchievements = self.hero.achievements;
const allAchievements = content.achievements;
for (const key of Object.keys(ownedAchievements)) {
const value = ownedAchievements[key];
if (typeof value === 'object') {
nestedAchievements[key] = [];
for (const nestedKey of Object.keys(value)) {
const valueIsInteger = self.integerTypes.includes(key);
let text = nestedKey;
if (allAchievements[key] && allAchievements[key][nestedKey]) {
text = getText(allAchievements[key][nestedKey]);
}
nestedAchievements[key].push({
key: nestedKey,
text,
achievementType: key,
modified: false,
path: `${basePath}.${key}.${nestedKey}`,
value: value[nestedKey],
valueIsInteger,
});
}
} else {
const valueIsInteger = self.integerTypes.includes(key);
achievements.push({
key,
text: getText(allAchievements[key]),
modified: false,
path: `${basePath}.${key}`,
value: ownedAchievements[key],
valueIsInteger,
});
}
}
const { textKey } = achievementItem;
if (textKey !== undefined) {
return i18n.t(textKey, { count });
for (const key of Object.keys(allAchievements)) {
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
if (ownedAchievements[key] === undefined) {
const valueIsInteger = self.integerTypes.includes(key);
achievements.push({
key,
text: getText(allAchievements[key]),
modified: false,
path: `${basePath}.${key}`,
value: valueIsInteger ? 0 : false,
valueIsInteger,
neverOwned: true,
});
}
}
}
const { singularTextKey } = achievementItem;
if (singularTextKey !== undefined) {
return i18n.t(singularTextKey, { count });
}
return '';
self.achievements = achievements;
self.nestedAchievements = nestedAchievements;
}
function resetData (self) {
collateItemData(self);
self.nestedAchievementKeys.forEach(itemType => { self.expandItemType[itemType] = false; });
}
export default {
@@ -192,34 +241,26 @@ export default {
},
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
],
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
'congrats', 'getwell'],
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests'],
achievements: [],
nestedAchievements: {},
};
},
watch: {
resetCounter () {
this.resetData();
resetData(this);
},
},
mounted () {
this.resetData();
resetData(this);
},
methods: {
async saveItem (item) {
await this.saveHero({
hero: {
_id: this.hero._id,
achievementPath: item.path,
achievementVal: item.value,
},
msg: item.path,
});
// prepare the item's new value and path for being saved
this.hero.achievementPath = item.path;
this.hero.achievementVal = item.value;
await this.saveHero({ hero: this.hero, msg: item.path });
item.modified = false;
},
enableValueChange (item) {
@@ -229,85 +270,6 @@ export default {
item.value = !item.value;
}
},
resetData () {
this.collateItemData();
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
},
collateItemData () {
const achievements = [];
const nestedAchievements = {};
const basePath = 'achievements';
const ownedAchievements = this.hero.achievements;
const allAchievements = content.achievements;
const ownedKeys = Object.keys(ownedAchievements).sort();
for (const key of ownedKeys) {
const value = ownedAchievements[key];
let contentKey = key;
if (this.cardTypes.indexOf(key) !== -1) {
contentKey += 'Cards';
}
if (typeof value === 'object') {
nestedAchievements[key] = [];
for (const nestedKey of Object.keys(value)) {
const valueIsInteger = this.integerTypes.includes(key);
let text = nestedKey;
if (allAchievements[key] && allAchievements[key][contentKey]) {
text = getText(allAchievements[key][contentKey]);
}
let notes = '';
if (allAchievements[key] && allAchievements[key][contentKey]) {
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
}
nestedAchievements[key].push({
key: nestedKey,
text,
notes,
achievementType: key,
modified: false,
path: `${basePath}.${key}.${nestedKey}`,
value: value[nestedKey],
valueIsInteger,
});
}
} else {
const valueIsInteger = this.integerTypes.includes(key);
achievements.push({
key,
text: getText(allAchievements[contentKey]),
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
modified: false,
path: `${basePath}.${key}`,
value: ownedAchievements[key],
valueIsInteger,
});
}
}
const allKeys = Object.keys(allAchievements).sort();
for (const key of allKeys) {
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
const ownedKey = key.replace('Cards', '');
if (ownedAchievements[ownedKey] === undefined) {
const valueIsInteger = this.integerTypes.includes(ownedKey);
achievements.push({
key: ownedKey,
text: getText(allAchievements[key]),
notes: getNotes(allAchievements[key], 0),
modified: false,
path: `${basePath}.${ownedKey}`,
value: valueIsInteger ? 0 : false,
valueIsInteger,
neverOwned: true,
});
}
}
}
this.achievements = achievements;
this.nestedAchievements = nestedAchievements;
},
},
};
</script>
@@ -1,12 +1,5 @@
<template>
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
contributor: hero.contributor,
secret: hero.secret,
permissions: hero.permissions,
}, msg: 'Contributor details', clearData: true })"
>
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
<div class="card mt-2">
<div class="card-header">
<h3
@@ -15,12 +8,6 @@
@click="expand = !expand"
>
Contributor Details
<b
v-if="hasUnsavedChanges && !expand"
class="text-warning float-right"
>
Unsaved changes
</b>
</h3>
</div>
<div
@@ -117,16 +104,13 @@
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
<b v-if="hasUnsavedChanges" class="text-warning float-right">
Unsaved changes
</b>
</div>
</div>
</form>
@@ -206,10 +190,6 @@ export default {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
},
data () {
return {
@@ -1,11 +1,5 @@
<template>
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
auth: hero.auth,
preferences: hero.preferences,
}, msg: 'Authentication' })"
>
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
<div class="card mt-2">
<div class="card-header">
<h3
@@ -62,12 +56,12 @@
<div class="col-sm-9 col-form-label">
<strong>
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
<a
<button
class="btn btn-warning btn-sm ml-4"
@click="resetCron()"
>
Reset Cron to Yesterday
</a>
</button>
</div>
</div>
<div class="form-group row">
@@ -119,14 +113,13 @@
<div class="form-group row">
<label class="col-sm-3 col-form-label">API Token</label>
<div class="col-sm-9">
<a
href="#"
<button
value="Change API Token"
class="btn btn-danger"
@click="changeApiToken()"
>
Change API Token
</a>
</button>
<div
v-if="tokenModified"
>
@@ -278,24 +271,13 @@ export default {
return false;
},
async changeApiToken () {
await this.saveHero({
hero: {
_id: this.hero._id,
changeApiToken: true,
},
msg: 'API Token',
});
this.hero.changeApiToken = true;
await this.saveHero({ hero: this.hero, msg: 'API Token' });
this.tokenModified = true;
},
resetCron () {
this.saveHero({
hero: {
_id: this.hero._id,
resetCron: true,
},
msg: 'Last Cron',
clearData: true,
});
this.hero.resetCron = true;
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
},
},
};
@@ -46,7 +46,7 @@
:
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
</span>
- {{ itemType }}.{{item.key}} - <i> {{ item.set }}</i>
{{ item.set }}
<div
v-if="item.modified"
@@ -232,14 +232,11 @@ export default {
},
methods: {
async saveItem (item) {
await this.saveHero({
hero: {
_id: this.hero._id,
purchasedPath: item.path,
purchasedVal: item.value,
},
msg: item.path,
});
// prepare the item's new value and path for being saved
this.hero.purchasedPath = item.path;
this.hero.purchasedVal = item.value;
await this.saveHero({ hero: this.hero, msg: item.path });
item.modified = false;
},
enableValueChange (item) {
@@ -15,17 +15,10 @@
<privileges-and-gems
:hero="hero"
:reset-counter="resetCounter"
:has-unsaved-changes="hasUnsavedChanges([hero.flags, unModifiedHero.flags],
[hero.auth, unModifiedHero.auth],
[hero.balance, unModifiedHero.balance],
[hero.secret, unModifiedHero.secret])"
/>
<subscription-and-perks
:hero="hero"
:group-plans="groupPlans"
:has-unsaved-changes="hasUnsavedChanges([hero.purchased.plan,
unModifiedHero.purchased.plan])"
/>
<cron-and-auth
@@ -36,7 +29,6 @@
<user-profile
:hero="hero"
:reset-counter="resetCounter"
:has-unsaved-changes="hasUnsavedChanges([hero.profile, unModifiedHero.profile])"
/>
<party-and-quest
@@ -55,12 +47,6 @@
:preferences="hero.preferences"
/>
<stats
:hero="hero"
:has-unsaved-changes="hasUnsavedChanges([hero.stats, unModifiedHero.stats])"
:reset-counter="resetCounter"
/>
<items-owned
:hero="hero"
:reset-counter="resetCounter"
@@ -81,18 +67,8 @@
:reset-counter="resetCounter"
/>
<user-history
:hero="hero"
:reset-counter="resetCounter"
/>
<contributor-details
:hero="hero"
:hasUnsavedChanges="hasUnsavedChanges(
[hero.contributor, unModifiedHero.contributor],
[hero.permissions, unModifiedHero.permissions],
[hero.secret, unModifiedHero.secret],
)"
:reset-counter="resetCounter"
@clear-data="clearData"
/>
@@ -133,7 +109,6 @@
</style>
<script>
import isEqualWith from 'lodash/isEqualWith';
import BasicDetails from './basicDetails';
import ItemsOwned from './itemsOwned';
import CronAndAuth from './cronAndAuth';
@@ -146,8 +121,6 @@ import Transactions from './transactions';
import SubscriptionAndPerks from './subscriptionAndPerks';
import CustomizationsOwned from './customizationsOwned.vue';
import Achievements from './achievements.vue';
import UserHistory from './userHistory.vue';
import Stats from './stats.vue';
import { userStateMixin } from '../../../mixins/userState';
@@ -162,8 +135,6 @@ export default {
PrivilegesAndGems,
ContributorDetails,
Transactions,
UserHistory,
Stats,
SubscriptionAndPerks,
UserProfile,
Achievements,
@@ -177,10 +148,8 @@ export default {
return {
userIdentifier: '',
resetCounter: 0,
unModifiedHero: {},
hero: {},
party: {},
groupPlans: [],
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
@@ -199,7 +168,6 @@ export default {
},
methods: {
clearData () {
this.unModifiedHero = {};
this.hero = {};
},
@@ -208,7 +176,6 @@ export default {
this.$emit('changeUserIdentifier', id); // change user identifier in Admin Panel's form
this.hero = await this.$store.dispatch('hall:getHero', { uuid: id });
this.unModifiedHero = JSON.parse(JSON.stringify(this.hero));
if (!this.hero.flags) {
this.hero.flags = {
@@ -239,38 +206,8 @@ export default {
}
}
if (this.hero.purchased.plan.planId === 'group_plan_auto') {
try {
this.groupPlans = await this.$store.dispatch('hall:getHeroGroupPlans', { heroId: this.hero._id });
} catch (e) {
this.groupPlans = [];
}
}
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
hasUnsavedChanges (...comparisons) {
for (const index in comparisons) {
if (index && comparisons[index]) {
const objs = comparisons[index];
const obj1 = objs[0];
const obj2 = objs[1];
if (!isEqualWith(obj1, obj2, (x, y) => {
if (typeof x === 'object' && typeof y === 'object') {
return undefined;
}
if (x === false && y === undefined) {
// Special case for checkboxes
return true;
}
return x == y; // eslint-disable-line eqeqeq
})) {
return true;
}
}
}
return false;
},
},
};
</script>
@@ -269,19 +269,16 @@ export default {
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
const toSave = {
_id: this.hero._id,
};
toSave.itemPath = item.path;
this.hero.itemPath = item.path;
if (item.value === null) {
toSave.itemVal = 'null';
this.hero.itemVal = 'null';
} else if (item.value === false) {
toSave.itemVal = 'false';
this.hero.itemVal = 'false';
} else {
toSave.itemVal = item.value;
this.hero.itemVal = item.value;
}
await this.saveHero({ hero: toSave, msg: item.key });
await this.saveHero({ hero: this.hero, msg: item.key });
item.neverOwned = false;
item.modified = false;
},
@@ -31,41 +31,22 @@
v-html="questErrors"
></p>
</div>
<div v-if="userHasParty">
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Party ID
</label>
<strong class="col-sm-9 col-form-label">
{{ groupPartyData._id }}
</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Estimated Member Count
</label>
<strong class="col-sm-9 col-form-label">
{{ groupPartyData.memberCount }}
</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Leader
</label>
<strong class="col-sm-9 col-form-label">
<div>
Party:
<span v-if="userHasParty">
yes: party ID {{ groupPartyData._id }},
member count {{ groupPartyData.memberCount }} (may be wrong)
<br>
<span v-if="userIsPartyLeader">User is the party leader</span>
<span v-else>Party leader is
<router-link :to="{'name': 'userProfile', 'params': {'userId': groupPartyData.leader}}">
{{ groupPartyData.leader }}
</router-link>
</span>
</strong>
</span>
<span v-else>no</span>
</div>
<div
class="btn btn-danger"
@click="removeFromParty()">Remove from Party</div>
</div>
<strong v-else>User is not in a party.</strong>
<div class="subsection-start">
<p v-html="questStatus"></p>
</div>
@@ -75,7 +56,6 @@
<script>
import * as quests from '@/../../common/script/content/quests';
import saveHero from '../mixins/saveHero';
function determineQuestStatus (self) {
// Quest data is in the user doc and party doc. They can be out of sync.
@@ -291,7 +271,6 @@ function resetData (self) {
}
export default {
mixins: [saveHero],
props: {
resetCounter: {
type: Number,
@@ -339,14 +318,5 @@ export default {
mounted () {
resetData(this);
},
methods: {
removeFromParty () {
this.saveHero({
hero: { _id: this.userId, removeFromParty: true },
msg: 'Removed from party',
reloadData: true,
});
},
},
};
</script>
@@ -1,11 +1,5 @@
<template>
<form @submit.prevent="saveHero({hero: {
_id: hero._id,
flags: hero.flags,
balance: hero.balance,
auth: hero.auth,
secret: hero.secret,
}, msg: 'Privileges or Gems or Moderation Notes'})">
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
<div class="card mt-2">
<div class="card-header">
<h3
@@ -14,9 +8,6 @@
@click="expand = !expand"
>
Privileges, Gem Balance
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
Unsaved changes
</b>
</h3>
</div>
<div
@@ -126,16 +117,13 @@
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
<b v-if="hasUnsavedChanges" class="text-warning float-right">
Unsaved changes
</b>
</div>
</div>
</form>
@@ -181,10 +169,6 @@ export default {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
},
data () {
return {
@@ -1,72 +0,0 @@
<template>
<div class="form-group row">
<label
class="col-sm-3 col-form-label"
:class="color"
>{{ label }}</label>
<div class="col-sm-9">
<input
:value="value"
class="form-control"
type="number"
:step="step"
:max="max"
:min="min"
@input="$emit('input', parseInt($event.target.value, 10))"
>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.about-row {
margin-left: 0px;
margin-right: 0px;
}
.red-label {
color: $red_100;
}
.blue-label {
color: $blue_100;
}
.purple-label {
color: $purple_300;
}
.yellow-label {
color: $yellow_50;
}
</style>
<script>
export default {
model: {
prop: 'value',
event: 'input',
},
props: {
label: {
type: String,
required: true,
},
color: {
type: String,
default: 'text-label',
},
value: {
type: Number,
required: true,
},
step: {
type: String,
default: 'any',
},
min: {
},
max: {
},
},
};
</script>
@@ -1,286 +0,0 @@
<template>
<form @submit.prevent="submitClicked()">
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Stats
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
Unsaved changes
</b>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<stats-row
label="Health"
color="red-label"
:max="maxHealth"
v-model="hero.stats.hp" />
<stats-row
label="Experience"
color="yellow-label"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.exp" />
<stats-row
label="Mana"
color="blue-label"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.mp" />
<stats-row
label="Level"
step="1"
min="0"
:max="maxLevelHardCap"
v-model="hero.stats.lvl" />
<stats-row
label="Gold"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.gp" />
<div class="form-group row">
<label class="col-sm-3 col-form-label">Selected Class</label>
<div class="col-sm-9">
<select
id="selectedClass"
v-model="hero.stats.class"
class="form-control"
:disabled="hero.stats.lvl < 10"
>
<option value="warrior">Warrior</option>
<option value="wizard">Mage</option>
<option value="healer">Healer</option>
<option value="rogue">Rogue</option>
</select>
<small>
When changing class, players usually need stat points deallocated as well.
</small>
</div>
</div>
<h3>Stat Points</h3>
<stats-row
label="Unallocated"
min="0"
step="1"
:max="maxStatPoints"
v-model="hero.stats.points" />
<stats-row
label="Strength"
color="red-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.str" />
<stats-row
label="Intelligence"
color="blue-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.int" />
<stats-row
label="Perception"
color="purple-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.per" />
<stats-row
label="Constitution"
color="yellow-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.con" />
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<button
type="button"
class="btn btn-warning btn-sm"
@click="deallocateStatPoints">
Deallocate all stat points
</button>
</div>
</div>
<div class="form-group row" v-if="statPointsIncorrect">
<div class="offset-sm-3 col-sm-9 text-danger">
Error: Sum of stat points should equal the users level
</div>
</div>
<h3>Buffs</h3>
<stats-row
label="Strength"
color="red-label"
min="0"
step="1"
v-model="hero.stats.buffs.str" />
<stats-row
label="Intelligence"
color="blue-label"
min="0"
step="1"
v-model="hero.stats.buffs.int" />
<stats-row
label="Perception"
color="purple-label"
min="0"
step="1"
v-model="hero.stats.buffs.per" />
<stats-row
label="Constitution"
color="yellow-label"
min="0"
step="1"
v-model="hero.stats.buffs.con" />
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<button
type="button"
class="btn btn-warning btn-sm"
@click="resetBuffs">
Reset Buffs
</button>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
<b v-if="hasUnsavedChanges" class="text-warning float-right">
Unsaved changes
</b>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.about-row {
margin-left: 0px;
margin-right: 0px;
}
</style>
<script>
import {
MAX_HEALTH,
MAX_STAT_POINTS,
MAX_LEVEL_HARD_CAP,
MAX_FIELD_HARD_CAP,
} from '@/../../common/script/constants';
import markdownDirective from '@/directives/markdown';
import saveHero from '../mixins/saveHero';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../../mixins/userState';
import StatsRow from './stats-row';
function resetData (self) {
self.expand = false;
}
export default {
directives: {
markdown: markdownDirective,
},
components: {
StatsRow,
},
mixins: [
userStateMixin,
saveHero,
],
computed: {
...mapState({ user: 'user.data' }),
statPointsIncorrect () {
if (this.hero.stats.lvl >= 10) {
return (parseInt(this.hero.stats.points, 10)
+ parseInt(this.hero.stats.str, 10)
+ parseInt(this.hero.stats.int, 10)
+ parseInt(this.hero.stats.per, 10)
+ parseInt(this.hero.stats.con, 10)
) !== this.hero.stats.lvl;
}
return false;
},
},
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
},
data () {
return {
expand: false,
maxHealth: MAX_HEALTH,
maxStatPoints: MAX_STAT_POINTS,
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
maxFieldHardCap: MAX_FIELD_HARD_CAP,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
submitClicked () {
if (this.statPointsIncorrect) {
return;
}
this.saveHero({
hero: {
_id: this.hero._id,
stats: this.hero.stats,
},
msg: 'Stats',
});
},
resetBuffs () {
this.hero.stats.buffs = {
str: 0,
int: 0,
per: 0,
con: 0,
};
},
deallocateStatPoints () {
this.hero.stats.points = this.hero.stats.lvl;
this.hero.stats.str = 0;
this.hero.stats.int = 0;
this.hero.stats.per = 0;
this.hero.stats.con = 0;
},
},
};
</script>
@@ -1,135 +1,30 @@
<template>
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
purchased: hero.purchased
}, msg: 'Subscription Perks' })"
>
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
<div class="card mt-2">
<div class="card-header"
@click="expand = !expand">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{ 'open': expand }"
@click="expand = !expand"
>
Subscription, Monthly Perks
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
Unsaved changes
</b>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Payment method:
</label>
<div class="col-sm-9">
<input v-model="hero.purchased.plan.paymentMethod"
class="form-control"
type="text"
v-if="!isRegularPaymentMethod"
>
<select
v-else
v-model="hero.purchased.plan.paymentMethod"
class="form-control"
type="text"
>
<option value="groupPlan">Group Plan</option>
<option value="Stripe">Stripe</option>
<option value="Apple">Apple</option>
<option value="Google">Google</option>
<option value="Amazon Payments">Amazon</option>
<option value="PayPal">PayPal</option>
<option value="Gift">Gift</option>
<option value="">Clear out</option>
</select>
</div>
<div v-if="hero.purchased.plan.paymentMethod">
Payment method:
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
</div>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Payment schedule:
</label>
<div class="col-sm-9">
<input v-model="hero.purchased.plan.planId"
class="form-control"
type="text"
v-if="!isRegularPlanId"
>
<select
v-else
v-model="hero.purchased.plan.planId"
class="form-control"
type="text"
>
<option value="basic_earned">Monthly recurring</option>
<option value="basic_3mo">3 Months recurring</option>
<option value="basic_6mo">6 Months recurring</option>
<option value="basic_12mo">12 Months recurring</option>
<option value="group_monthly">Group Plan (legacy)</option>
<option value="group_plan_auto">Group Plan (auto)</option>
<option value="">Clear out</option>
</select>
</div>
<div v-if="hero.purchased.plan.planId">
Payment schedule ("basic-earned" is monthly):
<strong>{{ hero.purchased.plan.planId }}</strong>
</div>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Customer ID:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.customerId"
class="form-control"
type="text"
>
</div>
</div>
<div class="form-group row"
v-if="hero.purchased.plan.planId === 'group_plan_auto'">
<label class="col-sm-3 col-form-label">
Group Plan Memberships:
</label>
<div class="col-sm-9 col-form-label">
<loading-spinner
v-if="!groupPlans"
dark-color=true
/>
<b
v-else-if="groupPlans.length === 0"
class="text-danger col-form-label"
>User is not part of an active group plan!</b>
<div
v-else
v-for="group in groupPlans"
:key="group._id"
class="card mb-2">
<div class="card-body">
<h6 class="card-title">{{ group.name }}
<small class="float-right">{{ group._id }}</small>
</h6>
<p class="card-text">
<strong>Leader: </strong>
<a
v-if="group.leader !== hero._id"
@click="switchUser(group.leader)"
>{{ group.leader }}</a>
<strong v-else class="text-success">This user</strong>
</p>
<p class="card-text">
<strong>Members: </strong> {{ group.memberCount }}
</p>
</div>
</div>
</div>
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
Group plan ID:
<strong>{{ hero.purchased.plan.owner }}</strong>
</div>
<div
v-if="hero.purchased.plan.dateCreated"
@@ -190,18 +85,8 @@
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
</strong>
<a class="btn btn-danger"
href="#"
v-b-modal.sub_termination_modal
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId">
Terminate
</a>
</div>
</div>
<small v-if="!hero.purchased.plan.dateTerminated
&& hero.purchased.plan.planId" class="text-success">
The subscription does not have a termination date and is active.
</small>
</div>
</div>
<div class="form-group row">
@@ -216,35 +101,6 @@
min="0"
step="1"
>
<small class="text-secondary">
Cumulative subscribed months across subscription periods.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Extra months:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.extraMonths"
class="form-control"
type="number"
min="0"
step="any"
>
<div class="input-group-append">
<a class="btn btn-warning"
@click="applyExtraMonths"
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0">
Apply Credit
</a>
</div>
</div>
<small class="text-secondary">
Additional credit that is applied if a subscription is cancelled.
</small>
</div>
</div>
<div class="form-group row">
@@ -318,6 +174,10 @@
>
</div>
</div>
<div v-if="hero.purchased.plan.extraMonths > 0">
Additional credit (applied upon cancellation):
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Mystery Items:
@@ -339,69 +199,23 @@
</span>
</div>
</div>
<div class="form-group row"
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'">
<div class="offset-sm-3 col-sm-9">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="beginGroupPlanConvert">
Begin converting to group plan subscription
</button>
</div>
</div>
<div class="form-group row"
v-if="isConvertingToGroupPlan">
<label class="col-sm-3 col-form-label">
Group Plan group ID:
</label>
<div class="col-sm-9">
<input
v-model="groupPlanID"
class="form-control"
type="text"
>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
@click="saveClicked"
>
<b v-if="hasUnsavedChanges" class="text-warning float-right">
Unsaved changes
</b>
</div>
</div>
<b-modal id="sub_termination_modal" title="Set Termination Date">
<p>
You can set the sub benefit termination date to today or to the last
day of the current billing cycle. Any extra subscription credit will
then be processed and automatically added onto the selected date.
</p>
<template #modal-footer>
<div class="mt-3 btn btn-secondary" @click="$bvModal.hide('sub_termination_modal')">
Close
</div>
<div class="mt-3 btn btn-danger" @click="terminateSubscription()">
Set to Today
</div>
<div class="mt-3 btn btn-danger" @click="terminateSubscription(todayWithRemainingCycle)">
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
</div>
</template>
</b-modal>
</form>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.input-group-append {
width: auto;
@@ -417,38 +231,21 @@
</style>
<script>
import isUUID from 'validator/es/lib/isUUID';
import moment from 'moment';
import { getPlanContext } from '@/../../common/script/cron';
import saveHero from '../mixins/saveHero';
import subscriptionBlocks from '../../../../../common/script/content/subscriptionBlocks';
import LoadingSpinner from '@/components/ui/loadingSpinner';
export default {
mixins: [saveHero],
components: {
LoadingSpinner,
},
props: {
hero: {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
groupPlans: {
type: Array,
default: null,
},
},
data () {
return {
expand: false,
isConvertingToGroupPlan: false,
groupPlanID: '',
subscriptionBlocks,
};
},
computed: {
@@ -458,30 +255,6 @@ export default {
if (!currentPlanContext.nextHourglassDate) return 'N/A';
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
},
isRegularPlanId () {
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
},
isRegularPaymentMethod () {
return [
'groupPlan',
'Group Plan',
'Stripe',
'Apple',
'Google',
'Amazon Payments',
'PayPal',
'Gift',
].includes(this.hero.purchased.plan.paymentMethod);
},
todayWithRemainingCycle () {
const now = moment();
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
while (terminationDate.isBefore(now)) {
terminationDate.add(monthCount, 'months');
}
return terminationDate;
},
},
methods: {
dateFormat (date) {
@@ -490,46 +263,6 @@ export default {
}
return moment(date).format('YYYY/MM/DD');
},
terminateSubscription (terminationDate) {
if (terminationDate) {
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
} else {
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
}
this.applyExtraMonths();
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
},
applyExtraMonths () {
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
const extraDays = Math.ceil(30.5 * extraMonths);
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
this.hero.purchased.plan.extraMonths = 0;
}
},
beginGroupPlanConvert () {
this.isConvertingToGroupPlan = true;
this.hero.purchased.plan.owner = '';
},
saveClicked (e) {
e.preventDefault();
if (this.isConvertingToGroupPlan) {
if (!isUUID(this.groupPlanID)) {
alert('Invalid group ID');
return;
}
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
} else {
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
}
},
switchUser (id) {
if (window.confirm('Switch to this user?')) {
this.$emit('changeUserIdentifier', id);
}
},
},
};
</script>
@@ -1,263 +0,0 @@
<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="toggleHistoryOpen"
>
User History
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div>
<div class="clearfix">
<div class="mb-4 float-left">
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'armoire'}"
@click="selectTab('armoire')"
>
Armoire
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'questInvites'}"
@click="selectTab('questInvites')"
>
Quest Invitations
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'cron'}"
@click="selectTab('cron')"
>
Cron
</button>
</div>
</div>
<div class="row">
<div
v-if="selectedTab === 'armoire'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th
v-once
>
Received
</th>
</tr>
<tr
v-for="entry in armoire"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.reward }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'questInvites'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th v-once>
Quest Key
</th>
<th v-once>
Response
</th>
</tr>
<tr
v-for="entry in questInviteResponses"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.quest }}</td>
<td>{{ questInviteResponseText(entry.response) }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'cron'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th v-once>
Checkin Count
</th>
</tr>
<tr
v-for="entry in cron"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.checkinCount }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.page-header.btn-flat {
background: transparent;
}
.tab-button {
height: 2rem;
font-size: 24px;
font-weight: bold;
font-stretch: condensed;
line-height: 1.33;
letter-spacing: normal;
color: $gray-10;
margin-right: 1.125rem;
padding-left: 0;
padding-right: 0;
padding-bottom: 2.5rem;
&.active, &:hover {
color: $purple-300;
box-shadow: 0px -0.25rem 0px $purple-300 inset;
outline: none;
}
}
</style>
<script>
import moment from 'moment';
import { userStateMixin } from '../../../mixins/userState';
export default {
filters: {
timeAgo (value) {
return moment(value).fromNow();
},
},
mixins: [userStateMixin],
props: {
hero: {
type: Object,
required: true,
},
resetCounter: {
type: Number,
required: true,
},
},
data () {
return {
expand: false,
selectedTab: 'armoire',
armoire: [],
questInviteResponses: [],
cron: [],
};
},
watch: {
resetCounter () {
if (this.expand) {
this.retrieveUserHistory();
}
},
},
methods: {
selectTab (type) {
this.selectedTab = type;
},
async toggleHistoryOpen () {
this.expand = !this.expand;
if (this.expand) {
this.retrieveUserHistory();
}
},
async retrieveUserHistory () {
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
this.armoire = history.armoire;
this.questInviteResponses = history.questInviteResponses;
this.cron = history.cron;
},
questInviteResponseText (response) {
if (response === 'accept') {
return 'Accepted';
}
if (response === 'reject') {
return 'Rejected';
}
if (response === 'leave') {
return 'Left active quest';
}
if (response === 'invite') {
return 'Accepted as owner';
}
if (response === 'abort') {
return 'Aborted by owner';
}
if (response === 'abortByLeader') {
return 'Aborted by party leader';
}
if (response === 'cancel') {
return 'Cancelled before start';
}
if (response === 'cancelByLeader') {
return 'Cancelled before start by party leader';
}
return response;
},
},
};
</script>
@@ -1,10 +1,5 @@
<template>
<form
@submit.prevent="saveHero({hero: {
_id: hero._id,
profile: hero.profile
}, msg: 'Users Profile'})"
>
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
<div class="card mt-2">
<div class="card-header">
<h3
@@ -13,9 +8,6 @@
@click="expand = !expand"
>
User Profile
<b v-if="hasUnsavedChanges && !expand" class="text-warning float-right">
Unsaved changes
</b>
</h3>
</div>
<div
@@ -59,16 +51,13 @@
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
<b v-if="hasUnsavedChanges" class="text-warning float-right">
Unsaved changes
</b>
</div>
</div>
</form>
@@ -112,10 +101,6 @@ export default {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
},
data () {
return {
+47 -46
View File
@@ -37,9 +37,9 @@
<h3>{{ $t('footerCompany') }}</h3>
<ul>
<li>
<a href="mailto:admin@habitica.com">
<router-link to="/static/contact">
{{ $t('contactUs') }}
</a>
</router-link>
</li>
<li>
<router-link to="/static/press-kit">
@@ -55,9 +55,9 @@
</li>
<li>
<a
@click="showBailey()"
>
{{ $t('oldNews') }}
href="https://habitica.fandom.com/wiki/Whats_New"
target="_blank"
>{{ $t('oldNews') }}
</a>
</li>
</ul>
@@ -80,7 +80,7 @@
</li>
<li>
<a
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
href="https://habitica.fandom.com/wiki/Contributing_to_Habitica"
target="_blank"
>{{ $t('companyContribute') }}
</a>
@@ -158,6 +158,13 @@
>{{ $t('guidanceForBlacksmiths') }}
</a>
</li>
<li>
<a
href="https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations"
target="_blank"
>{{ $t('communityExtensions') }}
</a>
</li>
</ul>
</div>
@@ -198,12 +205,12 @@
</a>
<a
class="social-circle"
href="https://bsky.app/profile/habitica.com"
href="https://twitter.com/habitica/"
target="_blank"
>
<div
class="social-icon svg-icon bluesky"
v-html="icons.bluesky"
class="social-icon svg-icon twitter"
v-html="icons.twitter"
></div>
</a>
<a
@@ -276,9 +283,9 @@
</div>
<div
class="time-travel"
v-if="TIME_TRAVEL_ENABLED && user?.permissions?.fullAccess"
:key="lastTimeJump"
class="time-travel"
>
<a
class="btn btn-secondary mr-1"
@@ -299,7 +306,7 @@
@click="resetTime()"
>
Reset
</a>
</a>
</div>
<a
class="btn btn-secondary mr-1"
@@ -403,7 +410,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.footer-row {
margin: 0;
flex: 0 1 auto;
@@ -511,7 +518,7 @@ footer {
background-color: $gray-500;
color: $gray-50;
padding: 32px 142px 40px;
a, a:not([href]) {
a {
color: $gray-50;
}
a:hover {
@@ -800,7 +807,7 @@ h3 {
}
}
.bluesky svg {
.twitter svg {
background-color: #e1e0e3;
fill: #878190;
height: 24px;
@@ -838,12 +845,12 @@ import moment from 'moment';
import Vue from 'vue';
// images
import melior from '@/assets/svg/melior.svg';
import bluesky from '@/assets/svg/bluesky.svg';
import facebook from '@/assets/svg/facebook.svg';
import instagram from '@/assets/svg/instagram.svg';
import tumblr from '@/assets/svg/tumblr.svg';
import heart from '@/assets/svg/heart.svg';
import melior from '@/assets/svg/melior.svg?raw';
import twitter from '@/assets/svg/twitter.svg?raw';
import facebook from '@/assets/svg/facebook.svg?raw';
import instagram from '@/assets/svg/instagram.svg?raw';
import tumblr from '@/assets/svg/tumblr.svg?raw';
import heart from '@/assets/svg/heart.svg?raw';
// components & modals
import { mapState } from '@/libs/store';
@@ -851,13 +858,13 @@ import buyGemsModal from './payments/buyGemsModal.vue';
import reportBug from '@/mixins/reportBug.js';
import { worldStateMixin } from '@/mixins/worldState';
const DEBUG_ENABLED = process.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
const TIME_TRAVEL_ENABLED = process.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true'; // eslint-disable-line no-process-env
const TIME_TRAVEL_ENABLED = import.meta.env.TIME_TRAVEL_ENABLED === 'true'; // eslint-disable-line no-process-env
let sinon;
if (TIME_TRAVEL_ENABLED) {
/* if (TIME_TRAVEL_ENABLED) {
// eslint-disable-next-line global-require
sinon = await import('sinon');
}
} */
export default {
components: {
@@ -871,7 +878,7 @@ export default {
return {
icons: Object.freeze({
melior,
bluesky,
twitter,
facebook,
instagram,
tumblr,
@@ -944,28 +951,24 @@ export default {
},
async jumpTime (amount) {
const response = await axios.post('/api/v4/debug/jump-time', { offsetDays: amount });
setTimeout(() => {
if (amount > 0) {
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
} else {
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
}
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
}, 1000);
if (amount > 0) {
Vue.config.clock.jump(amount * 24 * 60 * 60 * 1000);
} else {
Vue.config.clock.setSystemTime(moment().add(amount, 'days').toDate());
}
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
async resetTime () {
const response = await axios.post('/api/v4/debug/jump-time', { reset: true });
const time = new Date(response.data.data.time);
setTimeout(() => {
Vue.config.clock.restore();
Vue.config.clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
}, 1000);
Vue.config.clock.restore();
Vue.config.clock = sinon.useFakeTimers({
now: time,
shouldAdvanceTime: true,
});
this.lastTimeJump = response.data.data.time;
this.triggerGetWorldState(true);
},
addExp () {
// @TODO: Name these variables better
@@ -993,6 +996,7 @@ export default {
async bossRage () {
await axios.post('/api/v4/debug/boss-rage');
},
async makeAdmin () {
await axios.post('/api/v4/debug/make-admin');
// @TODO: Notification.text('You are now an admin!
@@ -1002,9 +1006,6 @@ export default {
donate () {
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
},
};
</script>
@@ -168,7 +168,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.form {
margin: 0 auto;
@@ -227,8 +227,8 @@ import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
import googleIcon from '@/assets/svg/google.svg?raw';
import appleIcon from '@/assets/svg/apple_black.svg?raw';
export default {
name: 'AuthForm',
@@ -290,7 +290,7 @@ export default {
},
mounted () {
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
methods: {
@@ -273,7 +273,7 @@
<div class="text-center">
<div>
<div
class="svg-icon habitica-logo"
class="svg-icon habitica-logo color"
v-html="icons.habiticaIcon"
></div>
</div>
@@ -355,7 +355,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
@media only screen and (min-height: 1080px) {
.bottom-wrap-register {
@@ -491,7 +491,7 @@
#top-background {
.seamless_stars_varied_opacity_repeat {
background-image: url('~@/assets/images/auth/seamless_stars_varied_opacity.png');
background-image: url('@/assets/images/auth/seamless_stars_varied_opacity.png');
background-repeat: repeat-x;
position: absolute;
height: 500px;
@@ -510,7 +510,7 @@
position: relative;
.seamless_mountains_demo_repeat {
background-image: url('~@/assets/images/auth/seamless_mountains_demo.png');
background-image: url('@/assets/images/auth/seamless_mountains_demo.png');
background-repeat: repeat-x;
width: 100%;
height: 300px;
@@ -520,7 +520,7 @@
}
.midground_foreground_extended2 {
background-image: url('~@/assets/images/auth/midground_foreground_extended2.png');
background-image: url('@/assets/images/auth/midground_foreground_extended2.png');
position: relative;
width: 1500px;
max-width: 100%;
@@ -611,11 +611,11 @@ import isEmail from 'validator/es/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { buildAppleAuthUrl } from '../../libs/auth';
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
import exclamation from '@/assets/svg/exclamation.svg';
import gryphon from '@/assets/svg/gryphon.svg';
import habiticaIcon from '@/assets/svg/logo-horizontal.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
import exclamation from '@/assets/svg/exclamation.svg?raw';
import gryphon from '@/assets/svg/gryphon.svg?raw';
import habiticaIcon from '@/assets/svg/logo-horizontal.svg?raw';
import googleIcon from '@/assets/svg/google.svg?raw';
import appleIcon from '@/assets/svg/apple_black.svg?raw';
export default {
mixins: [sanitizeRedirect],
@@ -726,13 +726,9 @@ export default {
},
mounted () {
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
if (this.forgotPassword) {
if (this.$route.query.email) {
this.username = this.$route.query.email;
}
}
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
methods: {
+102 -93
View File
@@ -1,103 +1,105 @@
<template>
<div
v-if="member.preferences"
class="avatar"
:style="{width, height, paddingTop}"
:class="topLevelClassList"
@click.prevent="castEnd()"
>
<div class="avatar-wrapper">
<div
class="character-sprites"
:style="{margin: spritesMargin}"
v-if="member.preferences"
class="avatar"
:style="{width, height, paddingTop}"
:class="topLevelClassList"
@click.prevent="castEnd()"
>
<template v-if="!avatarOnly">
<!-- Mount Body-->
<span
v-if="member.items.currentMount"
:class="'Mount_Body_' + member.items.currentMount"
></span>
</template>
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
<template v-for="(klass, item) in visualBuffs">
<span
v-if="member.stats.buffs[item] && showVisualBuffs"
:key="item"
:class="klass"
></span>
</template>
<!-- Show flower ALL THE TIME!!!-->
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
<!-- Show avatar only if not currently affected by visual buff-->
<template v-if="showAvatar()">
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
<span :class="[getGearClass('back'), specialMountClass]"></span>
<span :class="[skinClass, specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span
:class="[shirtClass, specialMountClass]"
></span>
<!-- eslint-enable max-len-->
<span :class="['head_0', specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
<!-- eslint-enable max-len-->
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
<template
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
>
<div
class="character-sprites"
:style="{margin: spritesMargin}"
>
<template v-if="!avatarOnly">
<!-- Mount Body-->
<span
:key="type"
:class="[hairClass(type), specialMountClass]"
v-if="member.items.currentMount"
:class="'Mount_Body_' + member.items.currentMount"
></span>
</template>
<span :class="[getGearClass('body'), specialMountClass]"></span>
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
<span :class="[getGearClass('head'), specialMountClass]"></span>
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
<!-- Buffs that cause visual changes to avatar: Snowman, Ghost, Flower, etc-->
<template v-for="(klass, item) in visualBuffs">
<span
v-if="member.stats.buffs[item] && showVisualBuffs"
:key="item"
:class="klass"
></span>
</template>
<!-- Show flower ALL THE TIME!!!-->
<!-- See https://github.com/HabitRPG/habitica/issues/7133-->
<span :class="'hair_flower_' + member.preferences.hair.flower"></span>
<!-- Show avatar only if not currently affected by visual buff-->
<template v-if="showAvatar()">
<span :class="['chair_' + member.preferences.chair, specialMountClass]"></span>
<span :class="[getGearClass('back'), specialMountClass]"></span>
<span :class="[skinClass, specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span
:class="[shirtClass, specialMountClass]"
></span>
<!-- eslint-enable max-len-->
<span :class="['head_0', specialMountClass]"></span>
<!-- eslint-disable max-len-->
<span :class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]"></span>
<!-- eslint-enable max-len-->
<span :class="[getGearClass('back_collar'), specialMountClass]"></span>
<template
v-for="type in ['bangs', 'base', 'mustache', 'beard']"
>
<span
:key="type"
:class="[hairClass(type), specialMountClass]"
></span>
</template>
<span :class="[getGearClass('body'), specialMountClass]"></span>
<span :class="[getGearClass('eyewear'), specialMountClass]"></span>
<span :class="[getGearClass('head'), specialMountClass]"></span>
<span :class="[getGearClass('headAccessory'), specialMountClass]"></span>
<span
:class="[
'hair_flower_' + member.preferences.hair.flower, specialMountClass
]"
></span>
<span
v-if="!hideGear('shield')"
:class="[getGearClass('shield'), specialMountClass]"
></span>
<span
v-if="!hideGear('weapon')"
:class="[getGearClass('weapon'), specialMountClass]"
class="weapon"
></span>
</template>
<!-- Resting-->
<span
:class="[
'hair_flower_' + member.preferences.hair.flower, specialMountClass
]"
v-if="member.preferences.sleep"
class="zzz"
></span>
<span
v-if="!hideGear('shield')"
:class="[getGearClass('shield'), specialMountClass]"
></span>
<span
v-if="!hideGear('weapon')"
:class="[getGearClass('weapon'), specialMountClass]"
class="weapon"
></span>
</template>
<!-- Resting-->
<span
v-if="member.preferences.sleep"
class="zzz"
></span>
<template v-if="!avatarOnly">
<!-- Mount Head-->
<span
v-if="member.items.currentMount"
:class="'Mount_Head_' + member.items.currentMount"
></span>
<!-- Pet-->
<span
class="current-pet"
:class="petClass"
></span>
</template>
<template v-if="!avatarOnly">
<!-- Mount Head-->
<span
v-if="member.items.currentMount"
:class="'Mount_Head_' + member.items.currentMount"
></span>
<!-- Pet-->
<span
class="current-pet"
:class="petClass"
></span>
</template>
</div>
<class-badge
v-if="hasClass && !hideClassBadge"
class="under-avatar"
:member-class="member.stats.class"
/>
</div>
<class-badge
v-if="hasClass && !hideClassBadge"
class="under-avatar"
:member-class="member.stats.class"
/>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.avatar {
width: 141px;
@@ -137,6 +139,11 @@
filter: invert(100%);
}
.weapon {
// the only one that is relative so that it fits into the parent div
position: relative !important;
}
.debug {
border: 1px solid red;
@@ -155,6 +162,7 @@
</style>
<script>
import some from 'lodash/some';
import moment from 'moment';
import { mapState } from '@/libs/store';
import foolPet from '../mixins/foolPet';
@@ -202,11 +210,11 @@ export default {
},
width: {
type: String,
default: '141px',
default: '140px',
},
height: {
type: String,
default: '147px',
default: undefined,
},
centerAvatar: {
type: Boolean,
@@ -321,10 +329,11 @@ export default {
return null;
},
petClass () {
const foolEvent = this.currentEventList?.find(event => moment()
.isBetween(event.start, event.end) && event.aprilFools);
if (foolEvent) {
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
if (some(
this.currentEventList,
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
)) {
return this.foolPet(this.member.items.currentPet);
}
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
return '';
@@ -27,7 +27,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.bottom-banner {
background: linear-gradient(114.26deg, $purple-300 0%, $purple-200 100%);
@@ -55,7 +55,7 @@
</style>
<script>
import sparkles from '@/assets/svg/sparkles-left.svg';
import sparkles from '@/assets/svg/sparkles-left.svg?raw';
export default {
data () {
@@ -42,8 +42,8 @@
</template>
<script>
import gem from '@/assets/svg/gem.svg';
import gold from '@/assets/svg/gold.svg';
import gem from '@/assets/svg/gem.svg?raw';
import gold from '@/assets/svg/gold.svg?raw';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import Sprite from '@/components/ui/sprite.vue';
@@ -72,7 +72,7 @@ export default {
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.customize-options {
width: 100%;
@@ -19,7 +19,7 @@ export default {
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.sub-menu {
display: flex;
@@ -31,7 +31,7 @@
<script>
import markdownDirective from '@/directives/markdown';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
const COMMUNITY_MANAGER_EMAIL = import.meta.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
directives: {
@@ -118,7 +118,7 @@
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h2 {
color: $white;
@@ -70,7 +70,7 @@
</style>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h2 {
color: $white;
@@ -134,7 +134,7 @@ label {
<script>
import closeIcon from '@/components/shared/closeIcon';
import checkCircleIcon from '@/assets/svg/check_circle.svg';
import checkCircleIcon from '@/assets/svg/check_circle.svg?raw';
import { MODALS } from '@/libs/consts';
export default {
@@ -259,7 +259,7 @@
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h1 {
color: $purple-200;
@@ -380,9 +380,9 @@ import sidebarSection from '../sidebarSection';
import userLink from '../userLink';
import groupLink from '../groupLink';
import gemIcon from '@/assets/svg/gem.svg';
import memberIcon from '@/assets/svg/member-icon.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gemIcon from '@/assets/svg/gem.svg?raw';
import memberIcon from '@/assets/svg/member-icon.svg?raw';
import calendarIcon from '@/assets/svg/calendar.svg?raw';
const TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
@@ -106,7 +106,7 @@
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
// Have to use this, because v-markdown creates p element in h3. Scoping doesn't work with it.
.challenge-title > p {
display: -webkit-box;
@@ -127,7 +127,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.challenge {
background-color: $white;
@@ -377,14 +377,14 @@ import categoryTags from '../categories/categoryTags';
import markdownDirective from '@/directives/markdown';
import { mapState } from '@/libs/store';
import gemIcon from '@/assets/svg/gem.svg';
import memberIcon from '@/assets/svg/member-icon.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import habitIcon from '@/assets/svg/habit.svg';
import todoIcon from '@/assets/svg/todo.svg';
import dailyIcon from '@/assets/svg/daily.svg';
import rewardIcon from '@/assets/svg/reward.svg';
import officialIcon from '@/assets/svg/official.svg';
import gemIcon from '@/assets/svg/gem.svg?raw';
import memberIcon from '@/assets/svg/member-icon.svg?raw';
import calendarIcon from '@/assets/svg/calendar.svg?raw';
import habitIcon from '@/assets/svg/habit.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw';
import dailyIcon from '@/assets/svg/daily.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw';
import officialIcon from '@/assets/svg/official.svg?raw';
export default {
components: {
@@ -207,7 +207,7 @@
</template>
<style lang='scss'>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#challenge-modal {
h5 {
@@ -81,7 +81,7 @@
</template>
<style lang='scss'>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#close-challenge-modal {
h2 {
@@ -98,7 +98,7 @@
}
.support-habitica {
background-image: url('~@/assets/svg/for-css/support-habitica-gems.svg');
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
width: 325px;
height: 89px;
margin: 0 auto;
@@ -63,7 +63,7 @@
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
@media only screen and (max-width: 768px) {
.header-row {
@@ -122,7 +122,7 @@ import challengeModal from './challengeModal';
import externalLinks from '@/mixins/externalLinks';
import challengeUtilities from '@/mixins/challengeUtilities';
import positiveIcon from '@/assets/svg/positive.svg';
import positiveIcon from '@/assets/svg/positive.svg?raw';
export default {
components: {
@@ -49,7 +49,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.no-challenge-section {
padding: 2em;
@@ -84,7 +84,7 @@ import markdownDirective from '@/directives/markdown';
import externalLinks from '../../mixins/externalLinks';
import challengeItem from './challengeItem';
import challengeIcon from '@/assets/svg/challenge.svg';
import challengeIcon from '@/assets/svg/challenge.svg?raw';
export default {
components: {
@@ -86,7 +86,7 @@
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
@media only screen and (max-width: 768px) {
.header-row {
@@ -150,8 +150,8 @@ import challengeModal from './challengeModal';
import challengeUtilities from '@/mixins/challengeUtilities';
import externalLinks from '@/mixins/externalLinks';
import challengeIcon from '@/assets/svg/challenge.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import challengeIcon from '@/assets/svg/challenge.svg?raw';
import positiveIcon from '@/assets/svg/positive.svg?raw';
export default {
components: {
@@ -102,7 +102,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.modal-body {
padding: 0px 8px 0px 8px;
@@ -207,8 +207,8 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import { userStateMixin } from '../../mixins/userState';
import markdownDirective from '@/directives/markdown';
import svgClose from '@/assets/svg/close.svg';
import svgReport from '@/assets/svg/report.svg';
import svgClose from '@/assets/svg/close.svg?raw';
import svgReport from '@/assets/svg/report.svg?raw';
export default {
directives: {
@@ -34,8 +34,8 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/tiers.scss';
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/tiers.scss';
@import '@/assets/scss/colors.scss';
.autocomplete-results {
padding: .5em;
@@ -74,16 +74,16 @@
<script>
import groupBy from 'lodash/groupBy';
import styleHelper from '@/mixins/styleHelper';
import tier1 from '@/assets/svg/tier-1.svg';
import tier2 from '@/assets/svg/tier-2.svg';
import tier3 from '@/assets/svg/tier-3.svg';
import tier4 from '@/assets/svg/tier-4.svg';
import tier5 from '@/assets/svg/tier-5.svg';
import tier6 from '@/assets/svg/tier-6.svg';
import tier7 from '@/assets/svg/tier-7.svg';
import tier8 from '@/assets/svg/tier-mod.svg';
import tier9 from '@/assets/svg/tier-staff.svg';
import tierNPC from '@/assets/svg/tier-npc.svg';
import tier1 from '@/assets/svg/tier-1.svg?raw';
import tier2 from '@/assets/svg/tier-2.svg?raw';
import tier3 from '@/assets/svg/tier-3.svg?raw';
import tier4 from '@/assets/svg/tier-4.svg?raw';
import tier5 from '@/assets/svg/tier-5.svg?raw';
import tier6 from '@/assets/svg/tier-6.svg?raw';
import tier7 from '@/assets/svg/tier-7.svg?raw';
import tier8 from '@/assets/svg/tier-mod.svg?raw';
import tier9 from '@/assets/svg/tier-staff.svg?raw';
import tierNPC from '@/assets/svg/tier-npc.svg?raw';
export default {
mixins: [styleHelper],
@@ -31,7 +31,6 @@
<avatar
v-if="user._id !== msg.uuid && msg.uuid !== 'system'"
class="avatar-left"
:height="null"
:class="{ invisible: avatarUnavailable(msg) }"
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
:avatar-only="true"
@@ -51,7 +50,6 @@
v-if="user._id === msg.uuid"
:class="{ invisible: avatarUnavailable(msg) }"
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
:height="null"
:avatar-only="true"
:hide-class-badge="true"
:override-top-padding="'14px'"
@@ -63,7 +61,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.avatar {
width: 10%;
@@ -98,6 +96,11 @@
}
}
.avatar-left {
margin-left: -1.5rem;
margin-right: 2rem;
}
.hr {
width: 100%;
height: 20px;
@@ -95,7 +95,7 @@
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.modal-body {
padding: 0px 8px 0px 8px;
@@ -199,7 +199,7 @@
import notifications from '@/mixins/notifications';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
import svgClose from '@/assets/svg/close.svg';
import svgClose from '@/assets/svg/close.svg?raw';
export default {
directives: {
+14 -14
View File
@@ -572,7 +572,7 @@
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
$dialogMarginTop: 56px;
$userCreationBgHeight: 105px;
@@ -671,7 +671,7 @@
}
.user-creation-bg {
background-image: url('~@/assets/creator/creator-hills-bg.png');
background-image: url('@/assets/creator/creator-hills-bg.png');
height: $userCreationBgHeight;
width: 219px;
margin: 0 auto;
@@ -1001,18 +1001,18 @@ import hairSettings from './avatarModal/hair-settings';
import extraSettings from './avatarModal/extra-settings';
import closeX from './ui/closeX';
import logoPurple from '@/assets/svg/logo-purple.svg';
import bodyIcon from '@/assets/svg/body.svg';
import accessoriesIcon from '@/assets/svg/accessories.svg';
import skinIcon from '@/assets/svg/skin.svg';
import hairIcon from '@/assets/svg/hair.svg';
import backgroundsIcon from '@/assets/svg/backgrounds.svg';
import gem from '@/assets/svg/gem.svg';
import hourglass from '@/assets/svg/hourglass.svg';
import gold from '@/assets/svg/gold.svg';
import arrowRight from '@/assets/svg/arrow_right.svg';
import arrowLeft from '@/assets/svg/arrow_left.svg';
import svgClose from '@/assets/svg/close.svg';
import logoPurple from '@/assets/svg/logo-purple.svg?raw';
import bodyIcon from '@/assets/svg/body.svg?raw';
import accessoriesIcon from '@/assets/svg/accessories.svg?raw';
import skinIcon from '@/assets/svg/skin.svg?raw';
import hairIcon from '@/assets/svg/hair.svg?raw';
import backgroundsIcon from '@/assets/svg/backgrounds.svg?raw';
import gem from '@/assets/svg/gem.svg?raw';
import hourglass from '@/assets/svg/hourglass.svg?raw';
import gold from '@/assets/svg/gold.svg?raw';
import arrowRight from '@/assets/svg/arrow_right.svg?raw';
import arrowLeft from '@/assets/svg/arrow_left.svg?raw';
import svgClose from '@/assets/svg/close.svg?raw';
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
import Sprite from './ui/sprite';
@@ -60,7 +60,7 @@
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
#external-link-modal {
&.modal {
@@ -174,8 +174,8 @@
</style>
<script>
import exclamationIcon from '@/assets/svg/exclamation.svg';
import closeIcon from '@/assets/svg/new-close.svg';
import exclamationIcon from '@/assets/svg/exclamation.svg?raw';
import closeIcon from '@/assets/svg/new-close.svg?raw';
export default {
data () {
+1 -1
View File
@@ -39,7 +39,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
.face-avatar {
width: 36px;
@@ -108,7 +108,7 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '@/assets/scss/colors.scss';
h2 {
color: $purple-300;
@@ -201,7 +201,7 @@
</style>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
@import '@/assets/scss/mixins.scss';
#create-group {
.modal-dialog {
max-width: 448px;

Some files were not shown because too many files have changed in this diff Show More