Merge branch 'release' into develop

This commit is contained in:
SabreCat 2023-10-09 13:31:50 -05:00
commit 71e565ea9a
158 changed files with 9132 additions and 4401 deletions

@ -1 +1 @@
Subproject commit 69724d88ef6a4be85104fb08ae1e3d0a3f649765
Subproject commit 335b47650fa5c4929c7c1680ed8960fe66cca266

1131
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.3.0",
"version": "5.7.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.5",
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.5",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
@ -17,7 +17,7 @@
"apple-auth": "^1.0.9",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.0",
"bootstrap": "^4.6.2",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"coupon-code": "^0.4.5",
@ -31,11 +31,12 @@
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.1.0",
"got": "^11.8.3",
"got": "^11.8.6",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"nodemon": "^2.0.20",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
@ -49,12 +50,12 @@
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"mongoose": "^5.13.20",
"morgan": "^1.10.0",
"nconf": "^0.12.0",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0",
@ -67,12 +68,12 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^12.9.0",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"validator": "^13.11.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.2.1",
@ -110,7 +111,7 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.3.6",
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",

View file

@ -0,0 +1,64 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /members/:memberId/clear-flags', () => {
let reporter;
let admin;
let moderator;
beforeEach(async () => {
reporter = await generateUser();
admin = await generateUser({ permissions: { userSupport: true } });
moderator = await generateUser({ permissions: { moderator: true } });
await reporter.post(`/members/${admin._id}/flag`);
});
context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(moderator.post('/members/gribbly/clear-flags'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();
await expect(moderator.post(`/members/${randomId}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});
it('returns error when requesting user is not a moderator', async () => {
await expect(reporter.post(`/members/${admin._id}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Only a moderator may clear reports from a profile.',
});
});
});
context('valid request', () => {
it('removes a single flag from user', async () => {
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});
it('removes multiple flags from user', async () => {
await moderator.post(`/members/${admin._id}/flag`);
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});
});
});

View file

@ -0,0 +1,151 @@
import { v4 as generateUUID } from 'uuid';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /members/:memberId/flag', () => {
let reporter;
let target;
beforeEach(async () => {
reporter = await generateUser();
target = await generateUser({
'profile.blurb': 'Naughty Text',
'profile.imageUrl': 'https://evil.com/',
});
});
context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(reporter.post('/members/gribbly/flag'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();
await expect(reporter.post(`/members/${randomId}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});
it('returns error when non-admin flags same profile twice', async () => {
await reporter.post(`/members/${target._id}/flag`);
await expect(reporter.post(`/members/${target._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'A profile can not be flagged more than once by the same user.',
});
});
});
context('valid request', () => {
let admin;
const comment = 'this profile is bad';
const source = 'Third Party Script';
beforeEach(async () => {
admin = await generateUser({ 'permissions.userSupport': true });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
});
afterEach(() => {
sandbox.restore();
});
it('adds flags object to target user', async () => {
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id]).to.have.all.keys([
'comment',
'source',
'timestamp',
]);
expect(moment(updatedTarget.profile.flags[reporter._id].timestamp).toDate()).to.be.a('date');
});
it('allows addition of a comment and source', async () => {
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id].comment).to.eql(comment);
expect(updatedTarget.profile.flags[reporter._id].source).to.eql(source);
});
it('allows moderator to flag twice', async () => {
const moderator = await generateUser({ 'permissions.moderator': true });
await moderator.post(`/members/${target._id}/flag`);
await expect(moderator.post(`/members/${target._id}/flag`)).to.eventually.be.ok;
});
it('allows multiple non-moderators to flag individually', async () => {
await admin.post(`/members/${target._id}/flag`);
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[admin._id]).to.exist;
expect(updatedTarget.profile.flags[reporter._id]).to.exist;
});
it('sends a flag report to moderation Slack', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${target.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${target._id}`,
text: `Display Name: ${target.profile.name}\n\nImage URL: ${target.profile.imageUrl}\n\nAbout: ${target.profile.blurb}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('excludes empty fields when sending Slack message', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${admin._id}/flag`, {
comment,
source,
});
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${admin.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${admin._id}`,
text: `Display Name: ${admin.profile.name}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
});
});

View file

@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
type: 'habit',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
type: 'daily',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
type: 'todo',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
type: 'reward',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -87,6 +95,26 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not allow to reset if the password is missing', async () => {
await expect(user.post('/user/reset', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('does not allow to reset if the password is wrong', async () => {
await expect(user.post('/user/reset', {
password: 'passdw',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const challenge = await generateChallenge(user, guild);
@ -102,7 +130,9 @@ describe('POST /user/reset', () => {
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await user.put('/user', {
@ -133,7 +163,9 @@ describe('POST /user/reset', () => {
},
});
await hero.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);

View file

@ -126,13 +126,5 @@ describe('shared.ops.addTask', () => {
expect(addTask(user)._editing).not.be.ok;
expect(addTask(user)._edit).to.not.be.ok;
});
it('respects advancedCollapsed preference', () => {
user.preferences.advancedCollapsed = true;
expect(addTask(user)._advanced).not.be.ok;
user.preferences.advancedCollapsed = false;
expect(addTask(user)._advanced).to.be.ok;
});
});
});

View file

@ -197,14 +197,14 @@ describe('shared.ops.purchase', () => {
it('purchases quest bundles', async () => {
const startingBalance = user.balance;
const clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
const clock = sandbox.useFakeTimers(moment('2022-03-16').valueOf());
const type = 'bundles';
const key = 'featheredFriends';
const key = 'cuddleBuddies';
const price = 1.75;
const questList = [
'falcon',
'harpy',
'owl',
'bunny',
'ferret',
'guineapig',
];
await purchase(user, { params: { type, key } });

View file

@ -15,6 +15,7 @@ module.exports = {
'import/no-unresolved': 'off',
'import/extensions': 'off',
'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
'vue/html-self-closing': ['error', {
html: {
void: 'never',

View file

@ -55,3 +55,13 @@ in a separate `.add('function of component', ...`
### Storybook Build
After each client build, storybook build is also triggered and will be available in `dist/storybook`
### Vue Structure
Currently pages and components are mixed in `/src/components` this is not a good way to find the files easy.
Thats why each changed / upcoming page / component should be put in either `/src/components` or in the `/src/pages` directory.
Inside Pages, each page can have a subfolder which contains sub-components only needed for that page - otherwise it has to be added to the normal components folder.
At the end of all the changes - the components should only contain components needed between all pages

View file

@ -13318,11 +13318,34 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"optional": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
@ -13354,6 +13377,38 @@
"ansi-regex": "^5.0.1"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -17346,7 +17401,7 @@
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
},
"debug": {
"version": "4.1.1",
@ -18568,13 +18623,29 @@
}
},
"eslint-plugin-vue": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz",
"integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.20.0.tgz",
"integrity": "sha512-oVNDqzBC9h3GO+NTgWeLMhhGigy6/bQaQbHS+0z7C4YEu/qK/yxHvca/2PTZtGNPsCrHwOTgKMrwu02A9iPBmw==",
"requires": {
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0",
"semver": "^5.6.0",
"vue-eslint-parser": "^7.0.0"
"semver": "^6.3.0",
"vue-eslint-parser": "^7.10.0"
},
"dependencies": {
"eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"eslint-scope": {
@ -21856,7 +21927,7 @@
"is-window": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
"integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0="
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg=="
},
"is-windows": {
"version": "1.0.2",
@ -30255,7 +30326,7 @@
"uuid-browser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz",
"integrity": "sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA="
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg=="
},
"v8-compile-cache": {
"version": "2.1.0",
@ -30549,29 +30620,90 @@
}
},
"vue-eslint-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz",
"integrity": "sha512-yR0dLxsTT7JfD2YQo9BhnQ6bUTLsZouuzt9SKRP7XNaZJV459gvlsJo4vT2nhZ/2dH9j3c53bIx9dnqU2prM9g==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz",
"integrity": "sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==",
"requires": {
"debug": "^4.1.1",
"eslint-scope": "^5.0.0",
"eslint-scope": "^5.1.1",
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.1.2",
"esquery": "^1.0.1",
"lodash": "^4.17.15"
"espree": "^6.2.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^6.3.0"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"requires": {
"esrecurse": "^4.1.0",
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
}
},
"espree": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
"integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
"requires": {
"acorn": "^7.1.1",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.1.0"
}
},
"esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"requires": {
"estraverse": "^5.1.0"
},
"dependencies": {
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
}
}
},
"esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"requires": {
"estraverse": "^5.2.0"
},
"dependencies": {
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
}
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vue-fragment": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A=="
},
"vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
@ -30606,76 +30738,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
"loader-utils": "^2.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^2.1.2"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-mugen-scroll": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/vue-mugen-scroll/-/vue-mugen-scroll-0.2.6.tgz",
@ -30706,6 +30768,340 @@
}
}
},
"vue-template-babel-compiler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-template-babel-compiler/-/vue-template-babel-compiler-2.0.0.tgz",
"integrity": "sha512-O0GOktQ5TZCZ5sWVl8CbyLBFriwwai7xDBtpdUI1xZSbbVVNf5Um/mDHYJXaHX6vfhmeAuohggXxIi0RPgXZ4g==",
"requires": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-proposal-optional-chaining": "^7.14.2",
"@babel/plugin-transform-arrow-functions": "^7.14.5",
"@babel/plugin-transform-block-scoping": "^7.14.5",
"@babel/plugin-transform-computed-properties": "^7.14.5",
"@babel/plugin-transform-destructuring": "^7.14.5",
"@babel/plugin-transform-parameters": "^7.14.5",
"@babel/plugin-transform-spread": "^7.14.5",
"@babel/types": "^7.14.5",
"deepmerge": "^4.2.2"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"requires": {
"@babel/highlight": "^7.18.6"
}
},
"@babel/compat-data": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
"integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ=="
},
"@babel/core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz",
"integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.2",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.1",
"@babel/parser": "^7.20.2",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.1",
"semver": "^6.3.0"
}
},
"@babel/generator": {
"version": "7.20.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz",
"integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==",
"requires": {
"@babel/types": "^7.20.2",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
}
},
"@babel/helper-compilation-targets": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
"integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
"requires": {
"@babel/compat-data": "^7.20.0",
"@babel/helper-validator-option": "^7.18.6",
"browserslist": "^4.21.3",
"semver": "^6.3.0"
}
},
"@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg=="
},
"@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"requires": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
}
},
"@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-module-imports": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-module-transforms": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
"integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
"requires": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
"@babel/helper-simple-access": "^7.20.2",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2"
}
},
"@babel/helper-plugin-utils": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ=="
},
"@babel/helper-simple-access": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
"requires": {
"@babel/types": "^7.20.2"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz",
"integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==",
"requires": {
"@babel/types": "^7.20.0"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
},
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
"integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="
},
"@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"requires": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
}
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz",
"integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg=="
},
"@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
"integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.6",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
}
},
"@babel/plugin-proposal-object-rest-spread": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz",
"integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==",
"requires": {
"@babel/compat-data": "^7.20.1",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-parameters": "^7.20.1"
}
},
"@babel/plugin-transform-arrow-functions": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz",
"integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.6"
}
},
"@babel/plugin-transform-block-scoping": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz",
"integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-computed-properties": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz",
"integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.9"
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz",
"integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-parameters": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz",
"integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-spread": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz",
"integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==",
"requires": {
"@babel/helper-plugin-utils": "^7.19.0",
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.9"
}
},
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
}
},
"@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"to-fast-properties": "^2.0.0"
}
},
"browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
"requires": {
"caniuse-lite": "^1.0.30001400",
"electron-to-chromium": "^1.4.251",
"node-releases": "^2.0.6",
"update-browserslist-db": "^1.0.9"
}
},
"caniuse-lite": {
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA=="
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
},
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vue-template-compiler": {
"version": "2.7.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz",

View file

@ -37,7 +37,7 @@
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
@ -58,9 +58,11 @@
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vue-template-compiler": "^2.7.10",
"vue-template-babel-compiler": "^2.0.0",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^4.47.0"

View file

@ -1439,6 +1439,11 @@
width: 141px;
height: 147px;
}
.background_jack_o_lantern_stacks {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_jack_o_lantern_stacks.png');
width: 141px;
height: 147px;
}
.background_jungle_canopy {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_jungle_canopy.png');
width: 141px;
@ -1564,6 +1569,11 @@
width: 141px;
height: 147px;
}
.background_monstrous_cave {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_monstrous_cave.png');
width: 141px;
height: 147px;
}
.background_mountain_lake {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_mountain_lake.png');
width: 141px;
@ -1884,6 +1894,11 @@
width: 141px;
height: 147px;
}
.background_spectral_candle_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_spectral_candle_room.png');
width: 141px;
height: 147px;
}
.background_spider_web {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_spider_web.png');
width: 141px;
@ -3200,6 +3215,11 @@
width: 68px;
height: 68px;
}
.icon_background_jack_o_lantern_stacks {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_jack_o_lantern_stacks.png');
width: 68px;
height: 68px;
}
.icon_background_jungle_canopy {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_jungle_canopy.png');
width: 68px;
@ -3325,6 +3345,11 @@
width: 68px;
height: 68px;
}
.icon_background_monstrous_cave {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_monstrous_cave.png');
width: 68px;
height: 68px;
}
.icon_background_mountain_lake {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_mountain_lake.png');
width: 68px;
@ -3645,6 +3670,11 @@
width: 68px;
height: 68px;
}
.icon_background_spectral_candle_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_spectral_candle_room.png');
width: 68px;
height: 68px;
}
.icon_background_spider_web {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_spider_web.png');
width: 68px;
@ -19130,6 +19160,11 @@
width: 114px;
height: 90px;
}
.head_armoire_blackSpookySorceryHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackSpookySorceryHat.png');
width: 114px;
height: 90px;
}
.head_armoire_blueFloppyHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blueFloppyHat.png');
width: 90px;
@ -19395,6 +19430,11 @@
width: 90px;
height: 90px;
}
.head_armoire_purpleSpookySorceryHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_purpleSpookySorceryHat.png');
width: 114px;
height: 90px;
}
.head_armoire_ramHeaddress {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_ramHeaddress.png');
width: 90px;
@ -20505,6 +20545,11 @@
width: 68px;
height: 68px;
}
.shop_head_armoire_blackSpookySorceryHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_blackSpookySorceryHat.png');
width: 68px;
height: 68px;
}
.shop_head_armoire_blueFloppyHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_blueFloppyHat.png');
width: 68px;
@ -20770,6 +20815,11 @@
width: 68px;
height: 68px;
}
.shop_head_armoire_purpleSpookySorceryHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_purpleSpookySorceryHat.png');
width: 68px;
height: 68px;
}
.shop_head_armoire_ramHeaddress {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_ramHeaddress.png');
width: 68px;
@ -21650,6 +21700,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_ridingBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_ridingBroom.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_sandySpade {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_sandySpade.png');
width: 68px;
@ -22600,6 +22655,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_ridingBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_ridingBroom.png');
width: 114px;
height: 90px;
}
.weapon_armoire_sandySpade {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_sandySpade.png');
width: 90px;
@ -23760,6 +23820,26 @@
width: 114px;
height: 90px;
}
.broad_armor_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2023Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2023Mage.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2023Rogue.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2023Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
width: 90px;
@ -23950,6 +24030,26 @@
width: 114px;
height: 90px;
}
.head_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2023Healer.png');
width: 114px;
height: 90px;
}
.head_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2023Mage.png');
width: 114px;
height: 90px;
}
.head_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2023Rogue.png');
width: 114px;
height: 90px;
}
.head_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2023Warrior.png');
width: 114px;
height: 90px;
}
.head_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
width: 90px;
@ -24090,6 +24190,21 @@
width: 114px;
height: 90px;
}
.shield_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2023Healer.png');
width: 114px;
height: 90px;
}
.shield_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2023Rogue.png');
width: 114px;
height: 90px;
}
.shield_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2023Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
width: 90px;
@ -24265,6 +24380,26 @@
width: 68px;
height: 68px;
}
.shop_armor_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2023Healer.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2023Mage.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2023Rogue.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2023Warrior.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fallHealer.png');
width: 68px;
@ -24455,6 +24590,21 @@
width: 68px;
height: 68px;
}
.shop_head_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2023Healer.png');
width: 68px;
height: 68px;
}
.shop_head_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2023Mage.png');
width: 68px;
height: 68px;
}
.shop_head_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2023Warrior.png');
width: 68px;
height: 68px;
}
.shop_head_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fallHealer.png');
width: 68px;
@ -24595,6 +24745,21 @@
width: 68px;
height: 68px;
}
.shop_shield_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2023Healer.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2023Rogue.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2023Warrior.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fallHealer.png');
width: 68px;
@ -24770,6 +24935,16 @@
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2023Healer.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2023Rogue.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fallHealer.png');
width: 68px;
@ -24790,6 +24965,21 @@
width: 68px;
height: 68px;
}
.shop_head_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2023Rogue.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2023Mage.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2023Warrior.png');
width: 68px;
height: 68px;
}
.slim_armor_special_fall2015Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2015Healer.png');
width: 93px;
@ -24950,6 +25140,26 @@
width: 114px;
height: 90px;
}
.slim_armor_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2023Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2023Mage.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2023Rogue.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2023Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
width: 90px;
@ -25130,6 +25340,26 @@
width: 114px;
height: 90px;
}
.weapon_special_fall2023Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2023Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2023Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2023Mage.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2023Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2023Rogue.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2023Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2023Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
width: 90px;
@ -28460,6 +28690,46 @@
width: 68px;
height: 68px;
}
.broad_armor_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202310.png');
width: 117px;
height: 120px;
}
.headAccessory_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_mystery_202310.png');
width: 117px;
height: 120px;
}
.head_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202310.png');
width: 117px;
height: 120px;
}
.shop_armor_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202310.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_mystery_202310.png');
width: 68px;
height: 68px;
}
.shop_head_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202310.png');
width: 68px;
height: 68px;
}
.shop_set_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202310.png');
width: 68px;
height: 68px;
}
.slim_armor_mystery_202310 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202310.png');
width: 117px;
height: 120px;
}
.broad_armor_mystery_301404 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
width: 90px;
@ -34474,214 +34744,6 @@
width: 68px;
height: 68px;
}
.headAccessory_special_bearEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_bearEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_bearEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_bearEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_blackHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_blackHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_blueHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_blueHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_cactusEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_cactusEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_cactusEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_cactusEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_foxEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_foxEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_foxEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_foxEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_greenHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_greenHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_heroicCirclet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_heroicCirclet.png');
width: 114px;
height: 90px;
}
.headAccessory_special_lionEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_lionEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_lionEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_lionEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pandaEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pandaEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_pandaEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pandaEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pigEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pigEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_pigEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pigEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pinkHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pinkHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_redHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_redHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_tigerEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_tigerEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_tigerEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_tigerEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_whiteHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_whiteHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_wolfEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_wolfEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_wolfEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_wolfEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_yellowHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_yellowHeadband.png');
width: 114px;
height: 90px;
}
.shop_headAccessory_special_bearEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_bearEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_blackHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_blackHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_blueHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_blueHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_cactusEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_cactusEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_foxEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_foxEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_greenHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_greenHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_heroicCirclet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_heroicCirclet.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_lionEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_lionEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pandaEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pandaEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pigEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pigEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pinkHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pinkHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_redHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_redHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_tigerEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_tigerEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_whiteHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_whiteHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_wolfEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_wolfEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_yellowHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_yellowHeadband.png');
width: 68px;
height: 68px;
}
.head_0 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_0.png');
width: 90px;
@ -35063,6 +35125,214 @@
width: 68px;
height: 68px;
}
.headAccessory_special_bearEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_bearEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_bearEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_bearEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_blackHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_blackHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_blueHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_blueHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_cactusEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_cactusEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_cactusEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_cactusEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_foxEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_foxEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_foxEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_foxEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_greenHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_greenHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_heroicCirclet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_heroicCirclet.png');
width: 114px;
height: 90px;
}
.headAccessory_special_lionEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_lionEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_lionEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_lionEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pandaEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pandaEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_pandaEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pandaEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pigEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pigEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_pigEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pigEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_pinkHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_pinkHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_redHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_redHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_tigerEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_tigerEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_tigerEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_tigerEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_whiteHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_whiteHeadband.png');
width: 114px;
height: 90px;
}
.headAccessory_special_wolfEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_wolfEars.png');
width: 90px;
height: 90px;
}
.customize-option.headAccessory_special_wolfEars {
background-position: -25px -15px;
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_wolfEars.png');
width: 60px;
height: 60px;
}
.headAccessory_special_yellowHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_special_yellowHeadband.png');
width: 114px;
height: 90px;
}
.shop_headAccessory_special_bearEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_bearEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_blackHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_blackHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_blueHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_blueHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_cactusEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_cactusEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_foxEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_foxEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_greenHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_greenHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_heroicCirclet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_heroicCirclet.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_lionEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_lionEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pandaEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pandaEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pigEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pigEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_pinkHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_pinkHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_redHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_redHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_tigerEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_tigerEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_whiteHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_whiteHeadband.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_wolfEars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_wolfEars.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_special_yellowHeadband {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_special_yellowHeadband.png');
width: 68px;
height: 68px;
}
.shield_healer_1 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_healer_1.png');
width: 90px;

View file

@ -5,15 +5,15 @@
font-weight: bold;
line-height: 1.71;
border: 1px solid transparent;
padding: 0.219rem 0.75rem;
border-radius: 2px;
padding: 4px 12px;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
color: $white;
&:hover, &:focus {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&:disabled, &.disabled, &.btn-flat {
&.btn-flat {
box-shadow: none;
}
}
@ -264,6 +264,10 @@
box-shadow: none;
}
.btn-cancel {
color: $blue-10;
}
.btn-small {
font-size: 12px;
line-height: 1.33;

View file

@ -7,6 +7,10 @@
.dropdown-toggle:hover {
--caret-color: #{$purple-300};
&.disabled {
pointer-events: none;
}
}
.dropdown.show > .dropdown-toggle:not(.btn-success) {
@ -136,6 +140,8 @@
.dropdown-menu.show {
min-width: 100% !important;
overflow: scroll;
max-height: 400px;
}
}

View file

@ -26,11 +26,11 @@ input, textarea, input.form-control, textarea.form-control {
color: $gray-50;
border: 1px solid $gray-400;
&:hover:not(:disabled) {
&:hover:not(:disabled):not(:read-only) {
border-color: $gray-300;
}
&:active:not(:disabled), &:focus:not(:disabled) {
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only) {
border-color: $purple-400;
outline: 0;
box-shadow: none;
@ -56,13 +56,13 @@ input, textarea, input.form-control, textarea.form-control {
&.input-valid, &.input-invalid {
background-repeat: no-repeat;
background-position: center right 16px;
background-position: center right 0.5rem;
}
&.input-valid {
padding-right: 37px;
padding-right: 27px;
background-image: url(~@/assets/svg/for-css/check.svg);
background-size: 13px 10px;
background-size: 1rem;
}
&.input-invalid {
@ -91,8 +91,10 @@ input, textarea, input.form-control, textarea.form-control {
border-color: $gray-300;
}
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
&:not(:read-only) {
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
}
}
.input-group-prepend , .input-group-append {
@ -163,8 +165,22 @@ input, textarea, input.form-control, textarea.form-control {
input {
height: 30px;
border: 0;
background: $white !important;
}
&.is-valid {
border-color: $green-10 !important;
}
&.is-invalid {
border-color: $red-100 !important;
}
}
.input-error {
font-size: 12px;
line-height: 1.33;
color: $maroon-10;
}
.input-group-spaced {
@ -231,20 +247,20 @@ $bg-disabled-control: $gray-10;
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
border-color: 2 px solid $purple-400;
background-color: $purple-400;
}
&:focus:disabled~.custom-control-label::before,
&:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
}
@ -398,8 +414,6 @@ $bg-color: $purple-400;
margin-top: 0 !important;
}
// Disable default style Firefox for invalid elements.
// Selectors taken from view-source:resource://gre-resources/forms.css on Firefox
:not(output):-moz-ui-invalid {

View file

@ -1,55 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000"
viewBox="0 0 1000 1187.198"
version="1.1"
height="1187.198"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="Apple_1998.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="705"
id="namedview6"
showgrid="false"
inkscape:zoom="0.1767767"
inkscape:cx="-1066.5045"
inkscape:cy="964.94669"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
id="path4"
inkscape:connector-curvature="0" />
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.841 2.564c-.567.672-1.474 1.202-2.382 1.126-.113-.908.331-1.873.851-2.47C7.877.53 8.87.039 9.673 0c.095.946-.274 1.873-.832 2.564zm.823 1.306c-1.314-.076-2.439.747-3.063.747-.633 0-1.588-.71-2.627-.69-1.352.018-2.609.785-3.299 2.005-1.418 2.441-.369 6.055 1.002 8.042.67.984 1.474 2.063 2.533 2.025 1.002-.038 1.399-.653 2.609-.653 1.219 0 1.569.653 2.627.634 1.097-.019 1.787-.984 2.458-1.968.765-1.116 1.077-2.204 1.096-2.261-.019-.019-2.117-.823-2.136-3.245-.019-2.025 1.654-2.99 1.73-3.047-.946-1.4-2.42-1.551-2.93-1.59z" fill="#1A181D" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 670 B

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#nw2v1izcda)">
<path d="m8 4.01 2.2 5.4 3.8-3.4-2 6H4l-2-6 3.8 3.4L8 4.01zm0-2c-.81 0-1.55.49-1.85 1.25L5.02 6.03 3.33 4.52A1.98 1.98 0 0 0 2 4.01 2.002 2.002 0 0 0 .1 6.64l2 6A2 2 0 0 0 4 14.01h8a2 2 0 0 0 1.9-1.37l1.97-5.9a1.997 1.997 0 0 0-1.85-2.73h-.04c-.5.01-.95.2-1.3.51L11 6.02 9.87 3.25A2.012 2.012 0 0 0 8.02 2L8 2.01z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="nw2v1izcda">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 617 B

View file

@ -1,3 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill="#24CC8F" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="vm46q29nca" d="M6.662 12.832c-.312 0-.61-.123-.831-.344L2 8.657l1.662-1.662 2.934 2.934L12.534 3l1.785 1.529-6.764 7.893c-.214.248-.521.396-.848.409l-.045.001"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g transform="translate(-306 -8) translate(306 8)">
<mask id="c8uzbxs4ob" fill="#fff">
<use xlink:href="#vm46q29nca"/>
</mask>
<use fill="#878190" xlink:href="#vm46q29nca"/>
<g fill="#20B780" mask="url(#c8uzbxs4ob)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 808 B

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="17"><defs><path id="a" d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z"/></defs><g transform="rotate(-90 8 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#BDA8FF" xlink:href="#a"/><g fill="#878190" mask="url(#b)"><path d="M0 0h16v16H0z"/></g></g></svg>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" >
<path d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z" id="myc95n2o6a"/>
</svg>

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 183 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd" opacity=".75" transform="translate(3 2)">
<path fill="#878190" d="M4 9h2V7H4v2zm4 1H2V6h6v4zM5 2c1.103 0 2 .897 2 2H3c0-1.103.897-2 2-2zm4 2.277V4c0-2.209-1.791-4-4-4S1 1.791 1 4v.277C.405 4.624 0 5.262 0 6v4c0 1.105.895 2 2 2h6c1.105 0 2-.895 2-2V6c0-.738-.405-1.376-1-1.723z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#464rik5fna)">
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zM9.41 7l1.29-1.29A.996.996 0 1 0 9.29 4.3L8 5.59 6.71 4.3A.996.996 0 1 0 5.3 5.71L6.59 7 5.3 8.29a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l1.29-1.29L9.3 9.7c.2.2.45.29.71.29.26 0 .51-.1.71-.29a.996.996 0 0 0 0-1.41L9.43 7h-.02z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="464rik5fna">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 770 B

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#mzh0y8jf2a)">
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zm-.17 7c0-.55-.45-1-1-1H5.17c-.55 0-1 .45-1 1s.45 1 1 1h5.66c.55 0 1-.45 1-1z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="mzh0y8jf2a">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View file

@ -731,6 +731,8 @@ export default {
},
},
mounted () {
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});

View file

@ -245,12 +245,13 @@ import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
import copyIcon from '@/assets/svg/copy.svg';
import copyToClipboard from '@/mixins/copyToClipboard';
export default {
components: {
closeX,
},
mixins: [notifications],
mixins: [notifications, copyToClipboard],
data () {
return {
icons: Object.freeze({
@ -287,17 +288,10 @@ export default {
this.$root.$emit('bv::hide::modal', 'create-party-modal');
},
copyUsername () {
if (navigator.clipboard) {
navigator.clipboard.writeText(this.user.auth.local.username);
} else {
const copyText = document.createElement('textarea');
copyText.value = this.user.auth.local.username;
document.body.appendChild(copyText);
copyText.select();
document.execCommand('copy');
document.body.removeChild(copyText);
}
this.text(this.$t('usernameCopied'));
this.mixinCopyToClipboard(
this.user.auth.local.username,
this.$t('usernameCopied'),
);
},
seekParty () {
this.$store.dispatch('user:set', {

View file

@ -86,11 +86,6 @@
color: $gray-50;
}
.input-error {
color: $red-50;
font-size: 90%;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;

View file

@ -84,7 +84,7 @@
v-if="invites.length > 0"
class="row"
>
<div class="col-6 offset-3 nav">
<div class="col-6 offset-3 nav mt-2 mb-3">
<div
class="nav-item"
:class="{active: selectedPage === 'members'}"
@ -111,17 +111,18 @@
:key="member._id"
class="row"
>
<div class="col-11 no-padding-left">
<div class="col-11 pl-0">
<member-details
:member="member"
:class-badge-position="'next-to-name'"
class="ml-4"
/>
</div>
<div class="col-1 actions">
<b-dropdown right="right">
<div
slot="button-content"
class="svg-icon inline dots"
class="svg-icon inline dots pt-1"
v-html="icons.dots"
></div>
<b-dropdown-item @click="sendMessage(member)">
@ -216,7 +217,7 @@
:key="member._id"
class="row"
>
<div class="col-11 no-padding-left">
<div class="col-11 pl-0">
<member-details :member="member" />
</div>
<div class="col-1 actions">
@ -259,10 +260,6 @@
color: #878190;
}
.no-padding-left {
padding-left: 0;
}
.modal-body {
padding-left: 0;
padding-right: 0;
@ -303,21 +300,15 @@
}
.actions {
padding-top: 5em;
.b-dropdown {
position: absolute;
right: 24px;
top: 8px;
}
.dots {
height: 16px;
width: 4px;
}
.btn-group {
margin-left: -2em;
margin-top: -2em;
}
.action-icon {
margin-right: 1em;
}
}
#members-modal_modal_body {
@ -353,8 +344,6 @@
.nav {
font-weight: bold;
margin-bottom: .5em;
margin-top: .5em;
}
.nav-item {

View file

@ -16,6 +16,7 @@
:class-badge-position="'next-to-name'"
:is-header="true"
:disable-name-styling="true"
class="mr-3"
/>
<div
v-if="hasParty"

View file

@ -3,6 +3,7 @@
<creator-intro />
<profileModal />
<report-flag-modal />
<report-member-modal />
<send-gift-modal />
<select-user-modal />
<b-navbar
@ -732,6 +733,7 @@ import creatorIntro from '../creatorIntro';
import notificationMenu from './notificationsDropdown';
import profileModal from '../userMenu/profileModal';
import reportFlagModal from '../chat/reportFlagModal';
import reportMemberModal from '../members/reportMemberModal';
import sendGiftModal from '@/components/payments/sendGiftModal';
import selectUserModal from '@/components/payments/selectUserModal';
import sync from '@/mixins/sync';
@ -744,6 +746,7 @@ export default {
notificationMenu,
profileModal,
reportFlagModal,
reportMemberModal,
sendGiftModal,
selectUserModal,
userDropdown,

View file

@ -117,14 +117,14 @@ import * as quests from '@/../../common/script/content/quests';
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
import notificationsIcon from '@/assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import MessageCount from './messageCount.functional.vue';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
import successImage from '@/assets/svg/success.svg';
import starBadge from '@/assets/svg/star-badge.svg';
// Notifications
import CARD_RECEIVED from './notifications/cardReceived';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation.functional.vue';
import GIFT_ONE_GET_ONE from './notifications/g1g1';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';

View file

@ -56,7 +56,7 @@
>{{ $t('achievements') }}</a>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'site'}"
:to="{name: 'general'}"
>
{{ $t('settings') }}
</router-link>
@ -141,7 +141,7 @@
import { mapState } from '@/libs/store';
import userIcon from '@/assets/svg/user.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import MessageCount from './messageCount.functional.vue';
import { EVENTS } from '@/libs/events';
import { PAGES } from '@/libs/consts';

View file

@ -231,7 +231,7 @@
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="'Pet_Egg_'+currentDraggingEgg.key"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div class="popover-content">
@ -248,7 +248,7 @@
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="'Pet_Egg_'+currentDraggingEgg.key"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div
@ -266,7 +266,7 @@
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
@ -285,7 +285,7 @@
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div

View file

@ -16,7 +16,7 @@
<span
v-drag.food="item.key"
class="item-content"
:class="'Pet_Food_'+item.key"
:class="`Pet_Food_${item.key}`"
@itemDragEnd="dragend($event)"
@itemDragStart="dragstart($event)"
></span>

View file

@ -6,10 +6,10 @@
>
<div class="potionEggGroup">
<div class="potionEggBackground">
<div :class="'Pet_HatchingPotion_'+hatchablePet.potionKey"></div>
<div :class="`Pet_HatchingPotion_${hatchablePet.potionKey}`"></div>
</div>
<div class="potionEggBackground">
<div :class="'Pet_Egg_'+hatchablePet.eggKey"></div>
<div :class="`Pet_Egg_${hatchablePet.eggKey}`"></div>
</div>
</div>
<h4 class="title">

View file

@ -268,7 +268,7 @@
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="'Pet_Food_'+currentDraggingFood.key"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
@ -287,7 +287,7 @@
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="'Pet_Food_'+currentDraggingFood.key"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div

View file

@ -1,10 +1,10 @@
<template>
<div
class="member-details"
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }"
class="member-details d-flex"
:class="{ condensed, expanded }"
@click="showMemberModal(member)"
>
<div class="avatar-container" :class="{ 'col-4': !isHeader }">
<div class="avatar-container">
<avatar
:member="member"
:hide-class-badge="classBadgePosition !== 'under-avatar'"
@ -15,14 +15,17 @@
</div>
<div
class="member-stats"
:class="{'col-8': !expanded && !isHeader}"
:class="{ 'mt-2': !isHeader }"
>
<div class="d-flex align-items-center profile-first-row">
<class-badge
v-if="classBadgePosition === 'next-to-name'"
:member-class="member.stats.class"
/>
<div class="d-flex flex-column profile-name-character">
<div
class="d-flex flex-column"
:class="{ 'ml-2': classBadgePosition === 'next-to-name' }"
>
<h3 class="character-name">
<span v-if="member.contributor && member.contributor.level > 0 && !disableNameStyling">
<user-link
@ -30,18 +33,22 @@
:name="member.profile.name"
:backer="member.backer"
:contributor="member.contributor"
:showBuffed="isBuffed"
:context="'profile'"
/>
</span>
<span v-else>{{ member.profile.name }}</span>
<div
v-if="isBuffed"
v-b-tooltip.hover.bottom="$t('buffed')"
class="is-buffed"
>
<div v-else>
<span>{{ member.profile.name }}</span>
<div
class="svg-icon"
v-html="icons.buff"
></div>
v-if="isBuffed"
v-b-tooltip.hover.bottom="$t('buffed')"
class="is-buffed ml-2 mt-n1"
>
<div
class="svg-icon"
v-html="icons.buff"
></div>
</div>
</div>
</h3>
<div class="small-text character-level">
@ -98,9 +105,12 @@
}
}
.standard-page .member-details {
padding-left: 24px;
}
.member-stats {
padding-left: 12px;
padding-right: 24px;
opacity: 1;
transition: width 0.15s ease-out;
}
@ -114,10 +124,6 @@
color: $header-color;
}
.profile-name-character {
margin-left: 12px;
}
.character-name {
margin-bottom: 1px;
color: $white;
@ -133,7 +139,6 @@
height: 20px;
background: $header-dark-background;
display: inline-block;
margin-left: 16px;
vertical-align: middle;
padding-top: 4px;

View file

@ -0,0 +1,199 @@
<template>
<b-modal
id="report-profile"
:title="$t('reportPlayer')"
:hide-footer="!hasPermission(user, 'moderator')"
size="md"
>
<div slot="modal-header">
<h2 class="mt-2 mb-0"> {{ $t('reportPlayer') }} </h2>
<close-x
@close="close()"
/>
</div>
<div>
<blockquote>
<strong> {{ displayName }} </strong>
<p class="mb-0"> {{ username }} </p>
</blockquote>
<div>
<strong>{{ $t('whyReportingPlayer') }}</strong>
<textarea
v-model="reportComment"
class="mt-2 form-control"
:placeholder="$t('whyReportingPlayerPlaceholder')"
></textarea>
</div>
<p
class="mb-2"
v-html="$t('playerReportModalBody', abuseFlagModalBody)">
</p>
</div>
<div class="footer text-center d-flex flex-column">
<button
class="btn btn-danger mx-auto mb-3"
:disabled="!reportComment"
:class="{ disabled: !reportComment }"
@click="reportAbuse()"
>
{{ $t('report') }}
</button>
<a
class="cancel-link"
@click.prevent="close()"
>{{ $t('cancel') }}</a>
</div>
<div
slot="modal-footer"
>
<div
class="d-flex"
@click="resetFlags()"
>
<div
v-once
class="svg-icon icon-16 color my-auto mr-2"
v-html="icons.report"
></div>
<a>Reset Flags</a>
</div>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#report-profile {
.modal-header {
padding: 24px;
border-bottom: none;
}
.modal-body {
padding: 0px 24px 24px 24px;
}
.modal-footer {
color: $maroon-50;
display: flex;
justify-content: center;
border-top: none;
height: 48px;
background-color: rgba($red-500, 0.25);
margin-top: -8px;
padding: 0px;
a {
margin-top: 2px;
color: $maroon-50;
}
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
strong, p {
line-height: 1.71;
}
h2 {
color: $maroon-100;
}
blockquote {
border-radius: 4px;
background-color: $gray-700;
padding: .5rem 1rem;
}
textarea {
margin-top: 1em;
margin-bottom: 1em;
border-radius: 2px;
border: solid 1px $gray-400;
min-height: 106px;
}
.footer {
padding: 1rem 1rem 0rem 1rem;
}
</style>
<script>
import closeX from '@/components/ui/closeX';
import notifications from '@/mixins/notifications';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
import report from '@/assets/svg/report.svg';
export default {
components: {
closeX,
},
directives: {
markdown: markdownDirective,
},
mixins: [notifications, userStateMixin],
data () {
const abuseFlagModalBody = {
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
secondLinkStart: '<a href="/static/terms" target="_blank">',
linkEnd: '</a>',
};
return {
abuseFlagModalBody,
displayName: '',
username: '',
reportComment: '',
icons: Object.freeze({
report,
}),
};
},
mounted () {
this.$root.$on('habitica::report-profile', this.handleReport);
},
beforeDestroy () {
this.$root.$off('habitica::report-profile', this.handleReport);
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'report-profile');
},
async reportAbuse () {
const result = await this.$store.dispatch('members:reportMember', {
memberId: this.memberId,
source: this.$route.fullPath,
comment: this.reportComment,
});
if (result.status === 200) {
this.text(this.$t('abuseReported'));
this.$root.$emit('habitica:report-profile-result', result.data.data);
} else {
this.error(result.statusText);
}
this.close();
},
handleReport (data) {
if (!data.memberId) return;
this.displayName = data.displayName;
this.username = `@${data.username}`;
this.memberId = data.memberId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-profile');
},
async resetFlags () {
const result = await this.$store.dispatch('members:clearMemberFlags', {
memberId: this.memberId,
});
if (result.status === 200) {
this.text('Flags cleared.');
} else {
this.err(result.statusText);
}
this.close();
},
},
};
</script>

View file

@ -171,7 +171,12 @@
</small>
<h4 class="mt-3 mx-auto"> {{ $t('limitations') }}</h4>
<small class="text-center">
{{ $t('gemSaleLimitations', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
{{ $t('gemSaleLimitations', {
eventStartMonth,
eventStartOrdinal,
eventEndMonth,
eventEndOrdinal,
}) }}
</small>
</div>
</div>
@ -441,6 +446,9 @@ export default {
eventStartOrdinal () {
return moment(this.currentEvent.start).format('Do');
},
eventEndMonth () {
return moment(this.currentEvent.end).format('MMMM');
},
eventEndOrdinal () {
return moment(this.currentEvent.end).format('Do');
},

View file

@ -22,18 +22,19 @@
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-margin d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
<div
v-once
class="svg-big-gift"
v-html="icons.bigGift"
></div>
</div>
<div
v-else
class="d-flex flex-column align-items-center">
v-else
class="d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
class="svg-big-gift"
v-html="icons.bigGift"
></div>
</div>
@ -49,9 +50,10 @@
></div>
</div>
<div
v-else
class="modal-close"
@click="close()">
v-else
class="modal-close"
@click="close()"
>
<div
class="svg-icon"
v-html="icons.close"
@ -65,26 +67,15 @@
name="selectUser"
novalidate="novalidate"
>
<div class="input-group">
<input
id="selectUser"
v-model="userSearchTerm"
class="form-control"
type="text"
ref="textBox"
:placeholder="$t('usernameOrUserId')"
:class="{
'input-valid': foundUser._id,
'is-invalid input-invalid': userNotFound,
}"
>
</div>
<div
v-if="userSearchTerm.length > 0 && userNotFound"
class="input-error text-center mt-2"
>
{{ $t('userWithUsernameOrUserIdNotFound') }}
</div>
<validated-text-input
id="selectUser"
v-model="userSearchTerm"
:is-valid="foundUser._id"
:placeholder="$t('usernameOrUserId')"
:invalid-issues="userInputInvalidIssues"
/>
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
<button
class="btn btn-primary mx-auto mt-2"
@ -104,16 +95,12 @@
</div>
</button>
<div
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
v-html="$t('cancel')"
@click="close()"
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
@click="close()"
v-html="$t('cancel')"
>
{{ $t('cancel') }}
</div>
<div
v-else>
</div>
</div>
</div>
</div>
</div>
@ -121,182 +108,179 @@
slot="modal-footer"
class="g1g1-fine-print text-center pt-3"
>
<strong>
{{ $t ('howItWorks') }}
<strong v-once>
{{ $t('howItWorks') }}
</strong>
<p
v-once
class="mx-5 mt-1"
>
{{ $t ('g1g1HowItWorks') }}
{{ $t('g1g1HowItWorks') }}
</p>
<strong>
{{ $t ('limitations') }}
<strong v-once>
{{ $t('limitations') }}
</strong>
<p
v-once
class="mx-5 mt-1"
>
{{ $t ('g1g1Limitations') }}
{{ $t('g1g1Limitations') }}
</p>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
@import '~@/assets/scss/mixins.scss';
#select-user-modal {
.modal-content {
width:448px;
#select-user-modal {
.modal-content {
width: 448px;
}
.input-group {
margin-top: 0rem;
}
.modal-dialog {
width: 448px;
}
.modal-footer {
padding: 0rem;
> * {
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
.input-group {
margin-top: 0rem;
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
.modal-dialog {
width: 448px;
}
.modal-footer {
padding: 0rem;
> * {
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
a:not([href]) {
font-size: 0.875rem;
line-height: 1.71;
}
a:not([href]) {
#selectUser {
width: 22rem;
border: 0px;
color: $gray-50;
}
font-size: 0.875rem;
line-height: 1.71;
}
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
#selectUser {
width: 22rem;
border: 0px;
color: $gray-50;
}
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
color: $white;
h1 {
font-size: 1.25rem;
line-height: 1.4;
color: $white;
h1 {
font-size: 1.25rem;
line-height: 1.4;
color: $white;
}
p {
font-size: 0.75rem;
line-height: 1.33;
margin-left: 4rem;
margin-right: 4rem;
margin-bottom: 0rem;
}
}
.g1g1-margin {
margin-top: 24px;
}
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
p {
font-size: 0.75rem;
line-height: 1.33;
margin-left: 4rem;
margin-right: 4rem;
margin-bottom: 0rem;
}
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-margin {
margin-top: 24px;
}
.g1g1-svg-icon {
width: 12px;
height: 12px;
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
& ::v-deep svg path {
fill: #FFFFFF;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
font-size: 0.75rem;
line-height: 1.33;
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-svg-icon {
width: 12px;
height: 12px;
& ::v-deep svg path {
fill: #FFFFFF;
}
}
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;
margin-top: 0.5rem;
}
.input-group:focus-within {
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.svg-icon {
width: 12px;
height: 12px;
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;
margin-top: 0.5rem;
}
.input-group:focus-within {
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.svg-icon {
width: 12px;
height: 12px;
}
}
</style>
@ -308,8 +292,10 @@ import isUUID from 'validator/lib/isUUID';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
import bigGiftIcon from '@/assets/svg/big-gift.svg';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
export default {
components: { ValidatedTextInput },
data () {
return {
userNotFound: false,
@ -332,6 +318,12 @@ export default {
if (this.userSearchTerm.length < 1) return true;
return typeof this.foundUser._id === 'undefined';
},
userInputInvalidIssues () {
return this.userSearchTerm.length > 0 && this.userNotFound
? [this.$t('userWithUsernameOrUserIdNotFound')]
: [''];
},
},
watch: {
userSearchTerm: {

View file

@ -1,190 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-6">
<h2>{{ $t('API') }}</h2>
<p>{{ $t('APIText') }}</p>
<div class="section">
<h6>{{ $t('userId') }}</h6>
<pre class="prettyprint">{{ user.id }}</pre>
<h6>{{ $t('APIToken') }}</h6>
<div class="d-flex align-items-center mb-3">
<button
class="btn btn-secondary"
@click="showApiToken = !showApiToken"
>
{{ $t(`${showApiToken ? 'hide' : 'show'}APIToken`) }}
</button>
<pre
v-if="showApiToken"
class="prettyprint ml-4 mb-0"
>{{ apiToken }}</pre>
</div>
<p v-html="$t('APITokenWarning', { hrefTechAssistanceEmail })"></p>
</div>
<div class="section">
<h3>{{ $t('thirdPartyApps') }}</h3>
<p v-html="$t('thirdPartyTools')"></p>
<hr>
</div>
</div>
<div class="col-6">
<h2>{{ $t('webhooks') }}</h2>
<p v-html="$t('webhooksInfo')"></p>
<table class="table table-striped">
<thead v-if="user.webhooks.length">
<tr>
<th>{{ $t('enabled') }}</th>
<th>{{ $t('webhookURL') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for="(webhook, index) in user.webhooks"
:key="webhook.id"
>
<td>
<input
v-model="webhook.enabled"
type="checkbox"
@change="saveWebhook(webhook, index)"
>
</td>
<td>
<input
v-model="webhook.url"
class="form-control"
type="url"
>
</td>
<td>
<div
class="btn btn-danger checklist-icons mr-2"
@click="deleteWebhook(webhook, index)"
>
<span
class="glyphicon glyphicon-trash"
:tooltip="$t('delete')"
> {{ $t('delete') }} </span>
</div>
<div
class="btn btn-primary checklist-icons"
@click="saveWebhook(webhook, index)"
>
{{ $t('subUpdateTitle') }}
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="form-horizontal">
<div class="form-group col-sm-10">
<input
v-model="newWebhook.url"
class="form-control"
type="url"
:placeholder="$t('webhookURL')"
>
</div>
<div class="col-sm-2">
<button
class="btn btn-sm btn-primary"
type="submit"
@click="addWebhook(newWebhook.url)"
>
{{ $t('add') }}
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.section {
margin-top: 2em;
}
li span
{
display: block;
}
</style>
<script>
import { mapState } from '@/libs/store';
import uuid from '@/../../common/script/libs/uuid';
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
export default {
data () {
return {
newWebhook: {
url: '',
},
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
showApiToken: false,
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
apiToken () {
return this.credentials.API_TOKEN;
},
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('API'),
});
window.addEventListener('message', this.receiveMessage, false);
},
destroy () {
window.removeEventListener('message', this.receiveMessage);
},
methods: {
receiveMessage (eventFrom) {
if (eventFrom.origin !== 'https://www.spritely.app') return;
const creds = {
userId: this.user._id,
apiToken: this.credentials.API_TOKEN,
};
eventFrom.source.postMessage(creds, eventFrom.origin);
},
async addWebhook (url) {
const webhookInfo = {
id: uuid(),
type: 'taskActivity',
options: {
created: false,
updated: false,
deleted: false,
scored: true,
},
url,
enabled: true,
};
const webhook = await this.$store.dispatch('user:addWebhook', { webhookInfo });
this.user.webhooks.push(webhook);
this.newWebhook.url = '';
},
async saveWebhook (webhook, index) {
delete webhook._editing;
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
},
async deleteWebhook (webhook, index) {
delete webhook._editing;
await this.$store.dispatch('user:deleteWebhook', { webhook });
this.user.webhooks.splice(index, 1);
},
},
};
</script>

View file

@ -1,26 +0,0 @@
<template>
<div class="row">
<div class="col-md-6">
<h2>{{ $t('dataExport') }}</h2>
<small>{{ $t('saveData') }}</small>
<h4>{{ $t('habitHistory') }}</h4>
{{ $t('exportHistory') }}
<a href="/export/history.csv">{{ $t('csv') }}</a>
<h4>{{ $t('userData') }}</h4>
{{ $t('exportUserData') }}
<a href="/export/userdata.xml">{{ $t('xml') }}</a>
<a href="/export/userdata.json">{{ $t('json') }}</a>
</div>
</div>
</template>
<script>
export default {
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('dataExport'),
});
},
};
</script>

View file

@ -1,132 +0,0 @@
<template>
<div>
<div>
<h5>{{ $t('dayStartAdjustment') }}</h5>
<div class="mb-4">
{{ $t('customDayStartInfo1') }}
</div>
<h3 v-once>{{ $t('adjustment') }}</h3>
<div class="form-horizontal">
<div class="form-group">
<div class="">
<select
v-model="newDayStart"
class="form-control"
>
<option
v-for="option in dayStartOptions"
:key="option.value"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</div>
<div>
<button
class="btn btn-primary full-width mt-3"
:disabled="newDayStart === user.preferences.dayStart"
@click="openDayStartModal()"
>
{{ $t('save') }}
</button>
</div>
</div>
</div>
</div>
<div class="form-horizontal">
<div class="form-group">
<small>
<p v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})"></p>
<p v-html="$t('timezoneInfo')"></p>
</small>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import moment from 'moment';
import getUtcOffset from '../../../../common/script/fns/getUtcOffset';
import { mapState } from '@/libs/store';
export default {
name: 'dayStartAdjustment',
data () {
const dayStartOptions = [];
for (let number = 0; number <= 12; number += 1) {
const meridian = number < 12 ? 'AM' : 'PM';
const hour = number % 12;
const timeWithMeridian = `(${hour || 12}:00 ${meridian})`;
const option = {
value: number,
name: `+${number} hours ${timeWithMeridian}`,
};
if (number === 0) {
option.name = `Default ${timeWithMeridian}`;
}
dayStartOptions.push(option);
}
return {
newDayStart: 0,
dayStartOptions,
};
},
mounted () {
this.newDayStart = this.user.preferences.dayStart;
},
computed: {
...mapState({
user: 'user.data',
}),
timezoneOffsetToUtc () {
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
return `UTC${offsetString}`;
},
dayStart () {
return this.user.preferences.dayStart;
},
},
methods: {
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v4/user/custom-day-start', {
dayStart: this.newDayStart,
});
// @TODO
// Notification.text(response.data.data.message);
},
openDayStartModal () {
const nextCron = this.calculateNextCron();
// @TODO: Add generic modal
if (!window.confirm(this.$t('sureChangeCustomDayStartTime', { time: nextCron }))) return; // eslint-disable-line no-alert
this.saveDayStart();
// $rootScope.openModal('change-day-start', { scope: $scope });
},
calculateNextCron () {
let nextCron = moment()
.hours(this.newDayStart)
.minutes(0)
.seconds(0)
.milliseconds(0);
const currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
},
},
};
</script>
<style scoped>
.full-width {
width: 100%;
}
</style>

View file

@ -1,86 +0,0 @@
<template>
<b-modal
id="delete"
:title="$t('deleteAccount')"
:hide-footer="true"
size="md"
>
<div class="modal-body">
<br>
<strong v-if="user.auth.local.has_password">{{ $t('deleteLocalAccountText') }}</strong>
<strong
v-if="!user.auth.local.has_password"
>{{ $t('deleteSocialAccountText', {magicWord: 'DELETE'}) }}</strong>
<div class="row mt-3">
<div class="col-6">
<input
v-model="password"
class="form-control"
type="password"
>
</div>
</div>
<div class="row mt-3">
<div
id="feedback"
class="col-12 form-group"
>
<label for="feedbackTextArea">{{ $t('feedback') }}</label>
<textarea
id="feedbackTextArea"
v-model="feedback"
class="form-control"
></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('neverMind') }}
</button>
<button
class="btn btn-danger"
:disabled="!password"
@click="deleteAccount()"
>
{{ $t('deleteDo') }}
</button>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
export default {
data () {
return {
password: '',
feedback: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'delete');
},
async deleteAccount () {
await axios.delete('/api/v4/user', {
data: {
password: this.password,
feedback: this.feedback,
},
});
localStorage.clear();
window.location.href = '/static/home';
this.$root.$emit('bv::hide::modal', 'delete');
},
},
};
</script>

View file

@ -1,158 +0,0 @@
<template>
<div class="row">
<secondary-menu class="col-12">
<router-link
class="nav-link"
:to="{name: 'site'}"
exact="exact"
:class="{'active': $route.name === 'site'}"
>
{{ $t('site') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'api'}"
:class="{'active': $route.name === 'api'}"
>
{{ $t('API') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'dataExport'}"
:class="{'active': $route.name === 'dataExport'}"
>
{{ $t('dataExport') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'promoCode'}"
:class="{'active': $route.name === 'promoCode'}"
>
{{ $t('promoCode') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'subscription'}"
:class="{'active': $route.name === 'subscription'}"
>
{{ $t('subscription') }}
</router-link>
<router-link
v-if="hasPermission(user, 'userSupport')"
class="nav-link"
:to="{name: 'transactions'}"
:class="{'active': $route.name === 'transactions'}"
>
{{ $t('transactions') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'notifications'}"
:class="{'active': $route.name === 'notifications'}"
>
{{ $t('notifications') }}
</router-link>
</secondary-menu>
<div
v-if="$route.name === 'subscription' && promo === 'g1g1'"
class="g1g1-banner d-flex justify-content-center"
@click="showSelectUser"
>
<div
v-once
class="svg-icon svg-gifts left-gift"
v-html="icons.gifts"
>
</div>
<div class="d-flex flex-column align-items-center text-center">
<strong
class="mt-auto mb-1"
> {{ $t('g1g1Event') }} </strong>
<p
class="mb-auto"
>
{{ $t('g1g1Details') }}
</p>
</div>
<div
v-once
class="svg-icon svg-gifts right-gift"
v-html="icons.gifts"
>
</div>
</div>
<div class="col-12">
<router-view />
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
strong {
font-size: 1rem;
line-height: 1.25;
}
.g1g1-banner {
color: $white;
width: 100%;
height: 5.75rem;
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
cursor: pointer;
}
.left-gift {
margin: auto 3rem auto auto;
}
.right-gift {
margin: auto auto auto 3rem;
filter: flipH;
transform: scaleX(-1);
}
.svg-gifts {
width: 3.5rem;
}
</style>
<script>
import find from 'lodash/find';
import { mapState } from '@/libs/store';
import SecondaryMenu from '@/components/secondaryMenu';
import gifts from '@/assets/svg/gifts-vertical.svg';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
SecondaryMenu,
},
mixins: [userStateMixin],
data () {
return {
icons: Object.freeze({
gifts,
}),
};
},
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
}),
currentEvent () {
return find(this.currentEventList, event => Boolean(event.promo));
},
promo () {
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
return this.currentEvent.promo;
},
},
methods: {
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');
},
},
};
</script>

View file

@ -1,143 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1>{{ $t('notifications') }}</h1>
</div>
<div class="col-12">
<div class="checkbox">
<label>
<input
v-model="user.preferences.pushNotifications.unsubscribeFromAll"
type="checkbox"
class="mr-2"
@change="set('pushNotifications', 'unsubscribeFromAll')"
>
<span>{{ $t('unsubscribeAllPush') }}</span>
</label>
</div>
<br>
<div class="checkbox">
<label>
<input
v-model="user.preferences.emailNotifications.unsubscribeFromAll"
type="checkbox"
class="mr-2"
@change="set('emailNotifications', 'unsubscribeFromAll')"
>
<span>{{ $t('unsubscribeAllEmails') }}</span>
</label>
</div>
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
</div>
<div class="col-8">
<table class="table">
<tr>
<td></td>
<th>
<span>{{ $t('email') }}</span>
</th>
<th>
<span>{{ $t('push') }}</span>
</th>
</tr>
<tr
v-for="notification in notificationsIds"
:key="notification"
>
<td>
<span>{{ $t(notification) }}</span>
</td>
<td>
<input
v-model="user.preferences.emailNotifications[notification]"
type="checkbox"
@change="set('emailNotifications', notification)"
>
</td>
<td v-if="onlyEmailsIds.indexOf(notification) === -1">
<input
v-model="user.preferences.pushNotifications[notification]"
type="checkbox"
@change="set('pushNotifications', notification)"
>
</td><td v-else>
&nbsp;
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
import { mapState } from '@/libs/store';
import notificationsMixin from '@/mixins/notifications';
export default {
mixins: [notificationsMixin],
data () {
return {
notificationsIds: Object.freeze([
'majorUpdates',
'onboarding',
'newPM',
'wonChallenge',
'giftedGems',
'giftedSubscription',
'invitedParty',
'invitedGuild',
'kickedGroup',
'questStarted',
'invitedQuest',
'importantAnnouncements',
'weeklyRecaps',
'subscriptionReminders',
]),
// list of email-only notifications
onlyEmailsIds: Object.freeze([
'kickedGroup',
'importantAnnouncements',
'weeklyRecaps',
'onboarding',
'subscriptionReminders',
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('notifications'),
});
// If ?unsubFrom param is passed with valid email type,
// automatically unsubscribe users from that email and
// show an alert
// A simple object to map the key stored in the db (user.preferences.emailNotification[key])
// to its string id but ONLY when the preferences' key and the string key don't match
const MAP_PREF_TO_EMAIL_STRING = {
importantAnnouncements: 'inactivityEmails',
};
const { unsubFrom } = this.$route.query;
if (unsubFrom) {
await this.$store.dispatch('user:set', {
[`preferences.emailNotifications.${unsubFrom}`]: false,
});
const emailTypeString = this.$t(MAP_PREF_TO_EMAIL_STRING[unsubFrom] || unsubFrom);
this.text(this.$t('correctlyUnsubscribedEmailType', { emailType: emailTypeString }));
}
},
methods: {
set (preferenceType, notification) {
const settings = {};
settings[`preferences.${preferenceType}.${notification}`] = this.user.preferences[preferenceType][notification];
this.$store.dispatch('user:set', settings);
},
},
};
</script>

View file

@ -1,117 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-md-6">
<h2>{{ $t('promoCode') }}</h2>
<div
class="form-inline"
role="form"
>
<input
v-model="couponCode"
class="form-control"
type="text"
:placeholder="$t('promoPlaceholder')"
>
<button
class="btn btn-primary"
@click="enterCoupon()"
>
{{ $t('submit') }}
</button>
</div>
<div>
<small>{{ $t('couponText') }}</small>
</div>
<div v-if="user.permissions.coupons">
<hr>
<h4>{{ $t('generateCodes') }}</h4>
<div
class="form"
role="form"
>
<div class="form-group">
<input
v-model="codes.event"
class="form-control"
type="text"
placeholder="Event code (eg, 'wondercon')"
>
</div>
<div class="form-group">
<input
v-model="codes.count"
class="form-control"
type="number"
placeholder="Number of codes to generate (eg, 250)"
>
</div>
<div class="form-group">
<button
class="btn btn-primary"
type="submit"
@click="generateCodes(codes)"
>
{{ $t('generate') }}
</button>
<a
class="btn btn-secondary"
:href="getCodesUrl"
>{{ $t('getCodes') }}</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
export default {
mixins: [notifications],
data () {
return {
codes: {
event: '',
count: '',
},
couponCode: '',
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
getCodesUrl () {
if (!this.user) return '';
return '/api/v4/coupons';
},
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('promoCode'),
});
},
methods: {
generateCodes () {
// $http.post(ApiUrl.get() + '/api/v2/coupons/generate/
// '+codes.event+'?count='+(codes.count || 1))
// .success(function(res,code){
// $scope._codes = {};
// if (code!==200) return;
// window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+
// '&apiToken='+User.settings.auth.apiToken;
// })
},
async enterCoupon () {
const code = await axios.post(`/api/v4/coupons/enter/${this.couponCode}`);
if (!code) return;
this.$store.state.user.data = code.data.data;
this.text(this.$t('promoCodeApplied'));
},
},
};
</script>

View file

@ -1,46 +0,0 @@
<template>
<b-modal
id="reset"
:title="$t('resetAccount')"
:hide-footer="true"
size="md"
>
<p>{{ $t('resetText1') }}</p>
<p>{{ $t('resetText2') }}</p>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('neverMind') }}
</button>
<button
class="btn btn-danger"
@click="reset()"
>
{{ $t('resetDo') }}
</button>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
export default {
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'reset');
},
async reset () {
await axios.post('/api/v4/user/reset');
this.$router.push('/');
setTimeout(() => window.location.reload(true), 100);
},
},
};
</script>

View file

@ -1,210 +0,0 @@
<template>
<b-modal
id="restore"
:title="$t('fixValues')"
:hide-footer="true"
size="lg"
>
<p>{{ $t('fixValuesText1') }}</p>
<p>{{ $t('fixValuesText2') }}</p>
<div class="form-horizontal">
<h3>{{ $t('stats') }}</h3>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('health') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.hp"
class="form-control"
type="number"
step="any"
data-for="stats.hp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('experience') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.exp"
class="form-control"
type="number"
step="any"
data-for="stats.exp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('gold') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.gp"
class="form-control"
type="number"
step="any"
data-for="stats.gp"
>
</div>
<!--input.form-control(type='number',
step="any", data-for='stats.gp', v-model='restoreValues.stats.gp',disabled)-->
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('mana') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.mp"
class="form-control"
type="number"
step="any"
data-for="stats.mp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('level') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.lvl"
class="form-control"
type="number"
data-for="stats.lvl"
>
</div>
</div>
<h3>{{ $t('achievements') }}</h3>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('fix21Streaks') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.achievements.streak"
class="form-control"
type="number"
data-for="achievements.streak"
>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-danger"
@click="close()"
>
{{ $t('discardChanges') }}
</button>
<button
class="btn btn-primary"
@click="restore()"
>
{{ $t('saveAndClose') }}
</button>
</div>
</b-modal>
</template>
<script>
import clone from 'lodash/clone';
import { MAX_LEVEL_HARD_CAP } from '@/../../common/script/constants';
import { mapState } from '@/libs/store';
export default {
data () {
return {
restoreValues: {
stats: {
hp: 0,
mp: 0,
gp: 0,
exp: 0,
lvl: 0,
},
achievements: {
streak: 0,
},
},
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.restoreValues.stats = clone(this.user.stats);
this.restoreValues.achievements.streak = clone(this.user.achievements.streak);
},
methods: {
close () {
this.validateInputs();
this.$root.$emit('bv::hide::modal', 'restore');
},
restore () {
if (!this.validateInputs()) {
return;
}
if (this.restoreValues.stats.lvl > MAX_LEVEL_HARD_CAP) {
this.restoreValues.stats.lvl = MAX_LEVEL_HARD_CAP;
}
const userChangedLevel = this.restoreValues.stats.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.stats.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) this.restoreValues.stats.exp = 0;
this.user.stats = clone(this.restoreValues.stats);
this.user.achievements.streak = clone(this.restoreValues.achievements.streak);
const settings = {
'stats.hp': Number(this.restoreValues.stats.hp),
'stats.exp': Number(this.restoreValues.stats.exp),
'stats.gp': Number(this.restoreValues.stats.gp),
'stats.lvl': Number(this.restoreValues.stats.lvl),
'stats.mp': Number(this.restoreValues.stats.mp),
'achievements.streak': Number(this.restoreValues.achievements.streak),
};
this.$store.dispatch('user:set', settings);
this.$root.$emit('bv::hide::modal', 'restore');
},
validateInputs () {
const canRestore = ['hp', 'exp', 'gp', 'mp'];
let valid = true;
for (const stat of canRestore) {
if (this.restoreValues.stats[stat] === ''
|| this.restoreValues.stats[stat] < 0
) {
this.restoreValues.stats[stat] = this.user.stats[stat];
valid = false;
}
}
const inputLevel = Number(this.restoreValues.stats.lvl);
if (this.restoreValues.stats.lvl === ''
|| !Number.isInteger(inputLevel)
|| inputLevel < 1) {
this.restoreValues.stats.lvl = this.user.stats.lvl;
valid = false;
}
const inputStreak = Number(this.restoreValues.achievements.streak);
if (this.restoreValues.achievements.streak === ''
|| !Number.isInteger(inputStreak)
|| inputStreak < 0) {
this.restoreValues.achievements.streak = this.user.achievements.streak;
valid = false;
}
return valid;
},
},
};
</script>

View file

@ -1,859 +0,0 @@
<template>
<div class="row standard-page">
<restore-modal />
<reset-modal />
<delete-modal />
<h1 class="col-12">
{{ $t('settings') }}
</h1>
<div class="col-sm-6">
<div class="sleep">
<h5>{{ $t('pauseDailies') }}</h5>
<h4>{{ $t('sleepDescription') }}</h4>
<ul>
<li v-once>
{{ $t('sleepBullet1') }}
</li>
<li v-once>
{{ $t('sleepBullet2') }}
</li>
<li v-once>
{{ $t('sleepBullet3') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
v-once
class="sleep btn btn-primary btn-block pause-button"
@click="toggleSleep()"
>
{{ $t('pauseDailies') }}
</button>
<button
v-if="user.preferences.sleep"
v-once
class="btn btn-secondary btn-block pause-button"
@click="toggleSleep()"
>
{{ $t('unpauseDailies') }}
</button>
</div>
<hr>
<div class="form-horizontal">
<h5>{{ $t('language') }}</h5>
<select
class="form-control"
:value="user.preferences.language"
@change="changeLanguage($event)"
>
<option
v-for="lang in availableLanguages"
:key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</select>
<small>
{{ $t('americanEnglishGovern') }}
<br>
<strong v-html="$t('helpWithTranslation')"></strong>
</small>
</div>
<hr>
<div class="form-horizontal">
<h5>{{ $t('dateFormat') }}</h5>
<select
v-model="user.preferences.dateFormat"
class="form-control"
@change="set('dateFormat')"
>
<option
v-for="dateFormat in availableFormats"
:key="dateFormat"
:value="dateFormat"
>
{{ dateFormat }}
</option>
</select>
</div>
<hr>
<div class="form-horizontal">
<div class="form-group">
<h5>{{ $t('audioTheme') }}</h5>
<select
v-model="user.preferences.sound"
class="form-control"
@change="changeAudioTheme"
>
<option
v-for="sound in availableAudioThemes"
:key="sound"
:value="sound"
>
{{ $t(`audioTheme_${sound}`) }}
</option>
</select>
</div>
<button
v-once
class="btn btn-primary btn-xs"
@click="playAudio"
>
{{ $t('demo') }}
</button>
</div>
<hr>
<div
v-if="hasClass"
class="form-horizontal"
>
<h5>{{ $t('characterBuild') }}</h5>
<h6 v-once>
{{ $t('class') + ': ' }}
<!-- @TODO: what is classText-->
<!-- span(v-if='classText') {{ classText }}&nbsp;-->
<button
v-once
class="btn btn-danger btn-xs"
@click="changeClassForUser(true)"
>
{{ $t('changeClass') }}
</button>
<small class="cost">
&nbsp; 3 {{ $t('gems') }}
<!-- @TODO add icon span.Pet_Currency_Gem1x.inline-gems-->
</small>
</h6>
<hr>
</div>
<div>
<div
class="checkbox"
id="preferenceAdvancedCollapsed"
>
<label>
<input
v-model="user.preferences.advancedCollapsed"
type="checkbox"
class="mr-2"
@change="set('advancedCollapsed')"
>
<span class="hint">
{{ $t('startAdvCollapsed') }}
</span>
<b-popover
target="preferenceAdvancedCollapsed"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('startAdvCollapsedPop')"
/>
</label>
</div>
<div
v-if="party.memberCount === 1"
class="checkbox"
id="preferenceDisplayInviteAtOneMember"
>
<label>
<input
v-model="user.preferences.displayInviteToPartyWhenPartyIs1"
type="checkbox"
class="mr-2"
@change="set('displayInviteToPartyWhenPartyIs1')"
>
<span class="hint">
{{ $t('displayInviteToPartyWhenPartyIs1') }}
</span>
</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.levelUp"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'levelUp')"
>
<label>{{ $t('suppressLevelUpModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.hatchPet"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'hatchPet')"
>
<label>{{ $t('suppressHatchPetModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.raisePet"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'raisePet')"
>
<label>{{ $t('suppressRaisePetModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.streak"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'streak')"
>
<label>{{ $t('suppressStreakModal') }}</label>
</div>
<hr>
<button
id="buttonShowBailey"
class="btn btn-primary mr-2 mb-2"
@click="showBailey()"
>
{{ $t('showBailey') }}
<b-popover
target="buttonShowBailey"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('showBaileyPop')"
/>
</button>
<button
id="buttonFCV"
class="btn btn-primary mr-2 mb-2"
@click="openRestoreModal()"
>
{{ $t('fixVal') }}
<b-popover
target="buttonFCV"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('fixValPop')"
/>
</button>
<button
v-if="user.preferences.disableClasses == true"
id="buttonEnableClasses"
class="btn btn-primary mb-2"
@click="changeClassForUser(false)"
>
{{ $t('enableClass') }}
<b-popover
target="buttonEnableClasses"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('enableClassPop')"
/>
</button>
<hr>
<day-start-adjustment />
</div>
</div>
<div class="col-sm-6">
<h2>{{ $t('registration') }}</h2>
<div class="panel-body">
<div>
<ul class="list-inline">
<li
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
>
<button
v-if="!user.auth[network.key].id && network.key !== 'facebook'"
class="btn btn-primary mb-2"
@click="socialAuth(network.key, user)"
>
{{ $t('registerWithSocial', {network: network.name}) }}
</button>
<button
v-if="!hasBackupAuthOption(network.key) && user.auth[network.key].id"
class="btn btn-primary mb-2"
disabled="disabled"
>
{{ $t('registeredWithSocial', {network: network.name}) }}
</button>
<button
v-if="hasBackupAuthOption(network.key) && user.auth[network.key].id"
class="btn btn-danger"
@click="deleteSocialAuth(network)"
>
{{ $t('detachSocial', {network: network.name}) }}
</button>
</li>
</ul>
<hr>
<div v-if="!user.auth.local.has_password">
<h5 v-if="!user.auth.local.email">
{{ $t('addLocalAuth') }}
</h5>
<h5 v-if="user.auth.local.email">
{{ $t('addPasswordAuth') }}
</h5>
<div
class="form"
name="localAuth"
novalidate="novalidate"
>
<div
v-if="!user.auth.local.email"
class="form-group"
>
<input
v-model="localAuth.email"
class="form-control"
type="text"
:placeholder="$t('email')"
required="required"
>
</div>
<div class="form-group">
<input
v-model="localAuth.password"
class="form-control"
type="password"
:placeholder="$t('password')"
required="required"
>
</div>
<div class="form-group">
<input
v-model="localAuth.confirmPassword"
class="form-control"
type="password"
:placeholder="$t('confirmPass')"
required="required"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="addLocalAuth()"
>
{{ $t('submit') }}
</button>
</div>
</div>
</div>
<div class="usersettings">
<h5>{{ $t('changeDisplayName') }}</h5>
<div
class="form"
name="changeDisplayName"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeDisplayname"
v-model="temporaryDisplayName"
class="form-control"
type="text"
:placeholder="$t('newDisplayName')"
:class="{'is-invalid input-invalid': displayNameInvalid}"
>
<div
v-if="displayNameIssues.length > 0"
class="mb-3"
>
<div
v-for="issue in displayNameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
</div>
</div>
<button
class="btn btn-primary"
type="submit"
:disabled="displayNameCannotSubmit"
@click="changeDisplayName(temporaryDisplayName)"
>
{{ $t('submit') }}
</button>
</div>
<h5>{{ $t('changeUsername') }}</h5>
<div
class="form"
name="changeUsername"
novalidate="novalidate"
>
<div
v-if="verifiedUsername"
class="iconalert iconalert-success"
>
{{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
</div>
<div
v-else
class="iconalert iconalert-warning"
>
<div class="align-middle">
<span>{{ $t('usernameNotVerified') }}</span>
</div>
</div>
<div class="form-group">
<input
id="changeUsername"
v-model="usernameUpdates.username"
class="form-control"
type="text"
:placeholder="$t('newUsername')"
:class="{'is-invalid input-invalid': usernameInvalid}"
@blur="restoreEmptyUsername()"
>
<div
v-for="issue in usernameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
<small class="form-text text-muted">{{ $t('changeUsernameDisclaimer') }}</small>
</div>
<button
class="btn btn-primary"
type="submit"
:disabled="usernameCannotSubmit"
@click="changeUser('username', usernameUpdates)"
>
{{ $t('saveAndConfirm') }}
</button>
</div>
<h5 v-if="user.auth.local.has_password">
{{ $t('changeEmail') }}
</h5>
<div
v-if="user.auth.local.email"
class="form"
name="changeEmail"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeEmail"
v-model="emailUpdates.newEmail"
class="form-control"
type="text"
:placeholder="$t('newEmail')"
>
</div>
<div
v-if="user.auth.local.has_password"
class="form-group"
>
<input
v-model="emailUpdates.password"
class="form-control"
type="password"
:placeholder="$t('password')"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="changeUser('email', emailUpdates)"
>
{{ $t('submit') }}
</button>
</div>
<h5 v-if="user.auth.local.has_password">
{{ $t('changePass') }}
</h5>
<div
v-if="user.auth.local.has_password"
class="form"
name="changePassword"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changePassword"
v-model="passwordUpdates.password"
class="form-control"
type="password"
:placeholder="$t('oldPass')"
>
</div>
<div class="form-group">
<input
v-model="passwordUpdates.newPassword"
class="form-control"
type="password"
:placeholder="$t('newPass')"
>
</div>
<div class="form-group">
<input
v-model="passwordUpdates.confirmPassword"
class="form-control"
type="password"
:placeholder="$t('confirmPass')"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="changeUser('password', passwordUpdates)"
>
{{ $t('submit') }}
</button>
</div>
<hr>
</div>
<div>
<h5>{{ $t('dangerZone') }}</h5>
<div>
<button
v-b-popover.hover.auto="$t('resetAccPop')"
class="btn btn-danger mr-2 mb-2"
popover-trigger="mouseenter"
popover-placement="right"
@click="openResetModal()"
>
{{ $t('resetAccount') }}
</button>
<button
v-b-popover.hover.auto="$t('deleteAccPop')"
class="btn btn-danger mb-2"
popover-trigger="mouseenter"
@click="openDeleteModal()"
>
{{ $t('deleteAccount') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
color: $gray-50;
}
.checkbox {
width: fit-content;
}
.usersettings h5 {
margin-top: 1em;
}
.iconalert > div > span {
line-height: 25px;
}
.iconalert > div:after {
clear: both;
content: '';
display: table;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
margin-top: 5px;
}
.sleep {
margin-bottom: 16px;
}
</style>
<script>
import hello from 'hellojs';
import axios from 'axios';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
import restoreModal from './restoreModal';
import resetModal from './resetModal';
import deleteModal from './deleteModal';
import dayStartAdjustment from './dayStartAdjustment';
import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
import changeClass from '@/../../common/script/ops/changeClass';
import notificationsMixin from '../../mixins/notifications';
import sounds from '../../libs/sounds';
import { buildAppleAuthUrl } from '../../libs/auth';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
export default {
components: {
restoreModal,
resetModal,
deleteModal,
dayStartAdjustment,
},
mixins: [notificationsMixin],
data () {
return {
SOCIAL_AUTH_NETWORKS: [],
party: {},
// Made available by the server as a script
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
temporaryDisplayName: '',
usernameUpdates: { username: '' },
emailUpdates: {},
passwordUpdates: {},
localAuth: {
username: '',
email: '',
password: '',
confirmPassword: '',
},
displayNameIssues: [],
usernameIssues: [],
};
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
availableAudioThemes () {
return ['off', ...this.content.audioThemes];
},
hasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
verifiedUsername () {
return this.user.flags.verifiedUsername;
},
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length <= 1) return false;
return this.displayNameIssues.length === 0;
},
displayNameCannotSubmit () {
if (this.temporaryDisplayName.length <= 1) return true;
return !this.displayNameValid;
},
usernameValid () {
if (this.usernameUpdates.username.length <= 1) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.usernameUpdates.username.length <= 1) return false;
return !this.usernameValid;
},
usernameCannotSubmit () {
if (this.usernameUpdates.username.length <= 1) return true;
return !this.usernameValid;
},
},
watch: {
usernameUpdates: {
handler () {
this.validateUsername(this.usernameUpdates.username);
},
deep: true,
},
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
},
mounted () {
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
// @TODO: We may need to request the party here
this.party = this.$store.state.party;
this.usernameUpdates.username = this.user.auth.local.username || null;
this.temporaryDisplayName = this.user.profile.name;
this.emailUpdates.newEmail = this.user.auth.local.email || null;
this.localAuth.username = this.user.auth.local.username || null;
this.soundIndex = 0;
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
});
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
}, {
redirect_uri: '', // eslint-disable-line
});
const focusID = this.$route.query.focus;
if (focusID !== undefined && focusID !== null) {
this.$nextTick(() => {
const element = document.getElementById(focusID);
if (element !== undefined && element !== null) {
element.focus();
}
});
}
},
methods: {
toggleSleep () {
this.$store.dispatch('user:sleep');
},
validateDisplayName: debounce(function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
set (preferenceType, subtype) {
const settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
return this.$store.dispatch('user:set', settings);
},
hideHeader () {
this.set('hideHeader');
if (!this.user.preferences.hideHeader || !this.user.preferences.stickyHeader) return;
this.user.preferences.hideHeader = false;
this.set('stickyHeader');
},
toggleStickyHeader () {
this.set('stickyHeader');
},
showTour () {
// @TODO: Do we still use this?
// User.set({'flags.showTour':true});
// Guide.goto('intro', 0, true);
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
hasBackupAuthOption (networkKeyToCheck) {
if (this.user.auth.local.username) {
return true;
}
return this.SOCIAL_AUTH_NETWORKS.find(network => {
if (network.key !== networkKeyToCheck) {
if (this.user.auth[network.key]) {
return !!this.user.auth[network.key].id;
}
}
return false;
});
},
async changeLanguage (e) {
const newLang = e.target.value;
this.user.preferences.language = newLang;
await this.set('language');
setTimeout(() => window.location.reload(true));
},
async changeUser (attribute, updates) {
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
if (attribute === 'username') {
this.user.auth.local.username = updates[attribute];
this.localAuth.username = this.user.auth.local.username;
this.user.flags.verifiedUsername = true;
} else if (attribute === 'email') {
this.user.auth.local.email = updates.newEmail;
window.alert(this.$t('emailSuccess')); // eslint-disable-line no-alert
} else if (attribute === 'password') {
this.passwordUpdates = {};
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('passwordSuccess'),
type: 'success',
timeout: true,
});
}
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', { 'profile.name': newName });
window.alert(this.$t('displayNameSuccess')); // eslint-disable-line no-alert
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
},
openRestoreModal () {
this.$root.$emit('bv::show::modal', 'restore');
},
openResetModal () {
this.$root.$emit('bv::show::modal', 'reset');
},
openDeleteModal () {
this.$root.$emit('bv::show::modal', 'delete');
},
async deleteSocialAuth (network) {
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
this.user.auth[network.key] = {};
this.text(this.$t('detachedSocial', { network: network.name }));
},
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
const auth = await hello(network).login({ scope: 'email' });
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
}
},
async changeClassForUser (confirmationNeeded) {
if (confirmationNeeded && !window.confirm(this.$t('changeClassConfirmCost'))) return; // eslint-disable-line no-alert
try {
changeClass(this.user);
await axios.post('/api/v4/user/change-class');
} catch (e) {
window.alert(e.message); // eslint-disable-line no-alert
}
},
async addLocalAuth () {
if (this.localAuth.email === '') {
this.localAuth.email = this.user.auth.local.email;
}
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
window.location.href = '/user/settings/site';
},
restoreEmptyUsername () {
if (this.usernameUpdates.username.length < 1) {
this.usernameUpdates.username = this.user.auth.local.username;
}
},
changeAudioTheme () {
this.soundIndex = 0;
this.set('sound');
},
playAudio () {
this.$root.$emit('playSound', sounds[this.soundIndex]);
this.soundIndex = (this.soundIndex + 1) % sounds.length;
},
},
};
</script>

View file

@ -115,8 +115,6 @@
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}

View file

@ -152,7 +152,7 @@
<button
v-if="getPriceClass() === 'gems'
&& !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
class="btn btn-primary"
class="btn btn-primary mb-3"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}

View file

@ -35,7 +35,7 @@
:key="item.key"
:item="item"
:price="item.value"
:item-content-class="'shop_'+item.key"
:item-content-class="`shop_${item.key}`"
:empty-item="false"
:popover-position="'top'"
@click="featuredItemSelected(item)"

View file

@ -0,0 +1,54 @@
<template>
<div
v-once
class="gem-price-div"
:class="{'background': withBackground}"
>
<div
:class="`mr-2 svg-icon gem icon-${iconSize}`"
v-html="icons.gem"
></div>
<span class="gem-price">{{ gemPrice }}</span>
</div>
</template>
<script>
import gemIcon from '@/assets/svg/gem.svg';
export default {
name: 'GemPrice',
props: ['gemPrice', 'iconSize', 'withBackground'],
data () {
return {
icons: Object.freeze({
gem: gemIcon,
}),
};
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.gem-price {
font-size: 20px;
font-weight: bold;
line-height: 1.4;
color: $green-10;
}
.gem-price-div {
display: inline-flex;
align-items: center;
}
.background {
align-self: center;
border-radius: 20px;
padding: 6px 20px;
background-color: rgba($green-100, 0.15);
}
</style>

View file

@ -265,25 +265,24 @@
align-items: center;
}
}
}
.how-many-to-sell {
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gold {
color: $yellow-5;
.how-many-to-sell {
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gold {
color: $yellow-5;
}
}
}
.total-text {
color: $gray-50;
@ -291,32 +290,31 @@
font-size: 0.875rem;
line-height: 1.71;
&.gold {
color: $yellow-5;
}
}
button.btn.btn-primary {
margin-top: 16px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
&.gold {
color: $yellow-5;
}
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
button.btn.btn-primary {
margin-top: 16px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
}
}
}
</style>
</style>
<script>
import svgClose from '@/assets/svg/close.svg';

View file

@ -92,7 +92,7 @@
:item="item"
:price="item.goldValue ? item.goldValue : item.value"
:price-type="item.goldValue ? 'gold' : 'gem'"
:item-content-class="'inventory_quest_scroll_'+item.key"
:item-content-class="`inventory_quest_scroll_${item.key}`"
:empty-item="false"
:popover-position="'top'"
@click="selectItem(item)"

View file

@ -82,7 +82,7 @@
<item-with-label
v-for="drop in getDropsList(quest.drop.items, false)"
:key="drop.type+'_'+drop.key"
:key="`${drop.type}_${drop.key}`"
:item="{}"
class="item-with-label"
>

View file

@ -346,6 +346,7 @@ import _sortBy from 'lodash/sortBy';
import _throttle from 'lodash/throttle';
import _groupBy from 'lodash/groupBy';
import _reverse from 'lodash/reverse';
import _find from 'lodash/find';
import { mapState } from '@/libs/store';
import Checkbox from '@/components/ui/checkbox';
@ -413,7 +414,7 @@ export default {
hidePinned: false,
featuredGearBought: false,
currentEvent: null,
backgroundUpdate: new Date(),
};
},
@ -422,7 +423,7 @@ export default {
content: 'content',
user: 'user.data',
userStats: 'user.data.stats',
currentEvent: 'worldState.data.currentEvent',
currentEventList: 'worldState.data.currentEventList',
}),
usersOfficalPinnedItems () {
@ -518,6 +519,7 @@ export default {
});
this.triggerGetWorldState();
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
},
beforeDestroy () {
this.$root.$off('buyModal::boughtItem');

View file

@ -273,6 +273,7 @@ import _sortBy from 'lodash/sortBy';
import _throttle from 'lodash/throttle';
import _groupBy from 'lodash/groupBy';
import _map from 'lodash/map';
import _find from 'lodash/find';
import { mapState } from '@/libs/store';
import ShopItem from '../shopItem';
@ -330,6 +331,8 @@ export default {
hidePinned: false,
backgroundUpdate: new Date(),
currentEvent: null,
};
},
computed: {
@ -339,7 +342,7 @@ export default {
user: 'user.data',
userStats: 'user.data.stats',
userItems: 'user.data.items',
currentEvent: 'worldState.data.currentEvent',
currentEventList: 'worldState.data.currentEventList',
}),
closed () {
@ -422,6 +425,7 @@ export default {
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
}
});
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
},
beforeDestroy () {
this.$root.$off('buyModal::boughtItem');

View file

@ -880,8 +880,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
this.$store.dispatch('common:setTitle', {

View file

@ -682,7 +682,9 @@ export default {
// as default filter for daily
// and set the filter as 'due' only when the component first
// loads and not on subsequent reloads.
if (type === 'daily' && filter === '' && !this.challenge) {
if (
type === 'daily' && filter === '' && !this.challenge
) {
filter = 'due'; // eslint-disable-line no-param-reassign
}

View file

@ -13,7 +13,6 @@
</span>
<label
v-once
class="mb-1"
v-html="text"
></label>
</div>
@ -23,11 +22,11 @@
@import '~@/assets/scss/colors.scss';
label {
height: 1.5rem;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
letter-spacing: normal;
margin: 0;
}
.gray-200 {

View file

@ -2,7 +2,6 @@
<div>
<select-list
:items="items"
:key-prop="'icon'"
class="difficulty-select"
:class="{disabled: disabled}"
:disabled="disabled"
@ -10,7 +9,7 @@
:hide-icon="true"
@select="$emit('select', $event.value)"
>
<template v-slot:item="{ item, button }">
<template #item="{ item, button }">
<div
v-if="item"
class="difficulty-item"

View file

@ -203,16 +203,15 @@
<template
v-if="task.type !== 'reward'"
>
<div class="d-flex mt-3">
<div class="d-flex mt-3 align-items-center">
<lockable-label
:locked="challengeAccessRequired"
:text="$t('difficulty')"
/>
<div
v-b-tooltip.hover.righttop="$t('difficultyHelp')"
class="svg-icon info-icon mb-auto ml-1"
v-html="icons.information"
></div>
<information-icon
tooltip-id="difficultyHelp"
:tooltip="$t('difficultyHelp')"
/>
</div>
<select-difficulty
:value="task.priority"
@ -452,7 +451,7 @@
>
<div>
<div
v-if="task.type === 'daily' && isUserTask && purpose === 'edit'"
v-if="advancedSettingsShowRestoreStreak"
class="option mt-3"
>
<div class="form-group">
@ -479,8 +478,7 @@
</div>
</div>
<div
v-if="task.type === 'habit'
&& isUserTask && purpose === 'edit' && (task.up || task.down)"
v-if="advancedSettingsShowAdjustCounter"
class="option mt-3"
>
<div class="form-group">
@ -539,6 +537,31 @@
</div>
</div>
</div>
<div
v-if="advancedSettingsShowTaskAlias"
class="option mt-3"
>
<div class="form-group">
<label
v-once
class="mb-1"
>{{ $t('taskAlias') }}
<information-icon
tooltip-id="taskAlias"
:tooltip="$t('taskAliasPopover')"
/>
</label>
<div class="input-group">
<input
v-model="task.alias"
class="form-control"
:placeholder="$t('taskAliasPlaceholder')"
type="text"
>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
@ -882,6 +905,11 @@
height: 1rem;
}
label {
display: inline-flex;
align-items: center;
}
.habit-option {
&-container {
min-width: 3rem;
@ -997,7 +1025,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
import streakIcon from '@/assets/svg/streak.svg';
@ -1006,10 +1033,12 @@ import goldIcon from '@/assets/svg/gold.svg';
import chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default {
components: {
InformationIcon,
SelectMulti,
Datepicker,
checklist,
@ -1029,7 +1058,6 @@ export default {
showAssignedSelect: false,
newChecklistItem: null,
icons: Object.freeze({
information: informationIcon,
negative: negativeIcon,
positive: positiveIcon,
destroy: deleteIcon,
@ -1064,25 +1092,25 @@ export default {
dayMapping: 'constants.DAY_MAPPING',
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
advancedSettingsAvailable () {
if (
this.task.type === 'reward'
|| this.task.type === 'todo'
|| this.purpose === 'create'
|| !this.isUserTask
) {
return false;
}
if (this.task.type === 'habit'
&& !this.task.up
&& !this.task.down
) {
return false;
}
return true;
// region advanced settings
advancedSettingsShowAdjustCounter () {
return this.task.type === 'habit'
&& this.isUserTask && this.purpose === 'edit'
&& (this.task.up || this.task.down);
},
advancedSettingsShowRestoreStreak () {
return this.task.type === 'daily' && this.isUserTask
&& this.purpose === 'edit';
},
advancedSettingsShowTaskAlias () {
return this.isUserTask && this.user.preferences.developerMode;
},
advancedSettingsAvailable () {
return this.advancedSettingsShowRestoreStreak
|| this.advancedSettingsShowAdjustCounter
|| this.advancedSettingsShowTaskAlias;
},
// endregion advanced settings
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
@ -1157,7 +1185,6 @@ export default {
},
},
async mounted () {
this.showAdvancedOptions = !this.user.preferences.advancedCollapsed;
if (this.groupId) {
const groupResponse = await axios.get(`/api/v4/groups/${this.groupId}`);
this.managers = Object.keys(groupResponse.data.data.managers);

View file

@ -486,6 +486,15 @@ export default {
this.$store.dispatch('common:setTitle', {
section: this.$t('tasks'),
});
if (this.$store.state.postLoadModal) {
const modalToLoad = this.$store.state.postLoadModal;
if (modalToLoad.includes('profile')) {
this.$router.push(modalToLoad);
} else {
this.$root.$emit('bv::show::modal', modalToLoad);
}
this.$store.state.postLoadModal = '';
}
},
methods: {
...mapActions({ setUser: 'user:set' }),

View file

@ -0,0 +1,37 @@
<template>
<span class="ml-1">
<div
:id="`tooltip_${tooltipId}`"
class="svg-icon icon-16"
:title="tooltip"
v-html="icons.information"
></div>
<b-tooltip
:title="tooltip"
:target="`tooltip_${tooltipId}`"
/>
</span>
</template>
<style lang="scss" scoped>
span {
display: inline-block;
vertical-align: middle;
}
</style>
<script>
import informationIcon from '@/assets/svg/information.svg';
export default {
name: 'InformationIcon',
props: ['tooltipId', 'tooltip'],
data () {
return {
icons: Object.freeze({
information: informationIcon,
}),
};
},
};
</script>

View file

@ -9,7 +9,7 @@
@show="isOpened = true"
@hide="isOpened = false"
>
<template v-slot:button-content>
<template #button-content>
<slot
name="item"
:item="selected || placeholder"
@ -21,13 +21,13 @@
</template>
<b-dropdown-item
v-for="item in items"
:key="keyProp ? item[keyProp] : item"
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]"
:active="item === selected"
:key="getKeyProp(item)"
:disabled="isDisabled(item)"
:active="isSelected(item)"
:class="{
active: item === selected,
active: isSelected(item),
selectListItem: true,
showIcon: !hideIcon && item === selected
showIcon: !hideIcon && isSelected(item)
}"
@click="selectItem(item)"
>
@ -51,39 +51,39 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
.select-list ::v-deep {
.dropdown-toggle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
.select-list ::v-deep {
.dropdown-toggle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
}
.selectListItem {
position: relative;
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.selectListItem {
position: relative;
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
&:not(.showIcon) {
.svg-icon.check-icon {
display: none;
}
}
&:not(.showIcon) {
.svg-icon.check-icon {
display: none;
}
}
.svg-icon.check-icon.color {
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
width: 0.77rem;
height: 0.615rem;
color: $purple-300;
}
.svg-icon.check-icon.color {
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
width: 0.77rem;
height: 0.615rem;
color: $purple-300;
}
}
}
</style>
<script>
@ -101,6 +101,9 @@ export default {
keyProp: {
type: String,
},
activeKeyProp: {
type: String,
},
disabledProp: {
type: String,
},
@ -128,10 +131,23 @@ export default {
};
},
methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item;
},
isDisabled (item) {
return typeof item[this.disabledProp] === 'undefined' ? false : item[this.disabledProp];
},
selectItem (item) {
this.selected = item;
this.selected = this.getKeyProp(item);
this.$emit('select', item);
},
isSelected (item) {
if (this.activeKeyProp) {
return item[this.activeKeyProp] === this.selected;
}
return item === this.selected;
},
},
};
</script>

View file

@ -0,0 +1,131 @@
<template>
<div>
<div class="label-line">
<div
v-if="settingsLabel"
class="settings-label"
>
{{ $t(settingsLabel) }}
</div>
<slot name="top-right"></slot>
</div>
<div class="form-group">
<div
class="input-group"
:class="{
'is-valid': validStyle,
'is-invalid': invalidStyle
}"
>
<input
:value="value"
class="form-control"
:type="inputType"
:class="{
'is-invalid input-invalid': invalidStyle,
'is-valid input-valid': validStyle
}"
:readonly="readonly"
:aria-readonly="readonly"
:placeholder="placeholder"
@keyup="handleChange"
@blur="$emit('blur')"
>
</div>
<div
v-for="issue in invalidIssues"
:key="issue"
class="input-error"
>
{{ issue }} &nbsp;
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ValidatedTextInput',
model: {
prop: 'value',
event: 'update:value',
},
props: {
value: {
type: String,
default: '',
},
isValid: {
type: Boolean,
default: false,
},
onlyShowInvalidState: {
type: Boolean,
default: false,
},
inputType: {
type: String,
default: 'text',
},
readonly: {
type: Boolean,
default: false,
},
settingsLabel: {
type: String,
},
placeholder: {
type: String,
},
invalidIssues: {
type: Array,
default: () => [],
},
},
data () {
return {
wasChanged: false,
};
},
computed: {
canChangeClasses () {
return !this.readonly && this.wasChanged;
},
validStyle () {
return this.canChangeClasses && this.isValid && !this.onlyShowInvalidState;
},
invalidStyle () {
return this.canChangeClasses && !this.isValid;
},
},
methods: {
handleChange ({ target: { value } }) {
this.wasChanged = true;
this.$emit('update:value', value);
},
},
};
</script>
<style lang="scss" scoped>
.label-line {
display: flex;
}
.settings-label {
flex: 1;
}
.input-error {
margin-top: 0.5rem;
}
.form-group {
margin-bottom: 0;
}
</style>

View file

@ -9,8 +9,19 @@
{{ displayName }}
<div
class="svg-icon icon-12"
:class="{ 'margin-bump': context === 'profile' }"
v-html="tierIcon()"
></div>
<div
v-if="showBuffed"
v-b-tooltip.hover.bottom="$t('buffed')"
class="is-buffed ml-2 d-flex align-items-center"
>
<div
class="svg-icon m-auto"
v-html="icons.buff"
></div>
</div>
</router-link>
</template>
@ -39,7 +50,12 @@
&[class*="tier"] .svg-icon {
margin-top: 5px;
&.margin-bump {
margin-top: 7px;
}
}
&.npc .svg-icon {
margin-top: 4px;
}
@ -52,6 +68,21 @@
}
}
}
.is-buffed {
width: 20px;
height: 20px;
background: $header-dark-background;
display: inline-block;
margin-top: 2px;
.svg-icon {
display: block;
width: 10px;
height: 12px;
margin: 0 auto;
}
}
</style>
<script>
@ -70,6 +101,7 @@ 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 buffIcon from '@/assets/svg/buff.svg';
export default {
mixins: [styleHelper],
@ -81,6 +113,8 @@ export default {
'contributor',
'hideTooltip',
'smallerStyle',
'showBuffed',
'context',
],
data () {
return {
@ -95,6 +129,7 @@ export default {
tier8,
tier9,
tierNPC,
buff: buffIcon,
}),
};
},

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,264 @@
<template>
<div
id="achievements"
class="standard-page"
>
<div
v-for="(category, key) in achievements"
:key="key"
class="row category-row"
>
<h3 class="text-center">
{{ $t(`${key}Achievs`) }}
</h3>
<div class="">
<div class="row achievements-row justify-content-center">
<div
v-for="(achievement, achievKey) in achievementsCategory(key, category)"
:key="achievKey"
class="achievement-wrapper col text-center"
>
<div
:id="achievKey + '-achievement'"
class="box achievement-container"
:class="{'achievement-unearned': !achievement.earned}"
>
<b-popover
:target="'#' + achievKey + '-achievement'"
triggers="hover"
placement="top"
>
<h4 class="popover-content-title">
{{ achievement.title }}
</h4>
<div
class="popover-content-text"
v-html="achievement.text"
></div>
</b-popover>
<div
v-if="achievement.earned"
class="achievement"
:class="achievement.icon + '2x'"
>
<div
v-if="achievement.optionalCount"
class="counter badge badge-pill stack-count"
>
{{ achievement.optionalCount }}
</div>
</div>
<div
v-if="!achievement.earned"
class="achievement achievement-unearned achievement-unearned2x"
></div>
</div>
</div>
</div>
<div
v-if="achievementsCategories[key].number > 5"
class="btn btn-flat btn-show-more"
@click="toggleAchievementsCategory(key)"
>
{{ achievementsCategories[key].open ?
$t('hideAchievements', {category: $t(`${key}Achievs`)}) :
$t('showAllAchievements', {category: $t(`${key}Achievs`)})
}}
</div>
</div>
</div>
<hr class="">
<div class="row">
<div
v-if="user.achievements.challenges"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-karaoke-2x"></div>
<h3 class="text-center">
{{ $t('challengesWon') }}
</h3>
<div
v-for="chal in user.achievements.challenges"
:key="chal"
class="achievement-list-item"
>
<span v-markdown="chal"></span>
</div>
</div>
<div
v-if="user.achievements.quests"
class="col-12 col-md-6"
>
<div class="achievement-icon achievement-alien2x"></div>
<h3 class="text-center">
{{ $t('questsCompleted') }}
</h3>
<div
v-for="(value, key) in user.achievements.quests"
:key="key"
class="achievement-list-item d-flex justify-content-between"
>
<span>{{ content.quests[key].text() }}</span>
<span
v-if="value > 1"
class="badge badge-pill stack-count"
>
{{ value }}
</span>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
#achievements {
.category-row {
margin-bottom: 34px;
justify-content: center;
&:last-child {
margin-bottom: 0px;
}
}
.achievements-row {
margin: 0 auto;
max-width: 590px;
}
.achievement-wrapper {
margin-left: 12px;
margin-right: 12px;
max-width: 94px;
min-width: 94px;
padding: 0px;
width: 94px;
}
.box {
background: $white;
margin: 0 auto;
margin-bottom: 16px;
padding-top: 20px;
}
hr {
margin-bottom: 48px;
margin-top: 48px;
}
.box.achievement-unearned {
background-color: $gray-600;
}
.counter.badge {
background-color: $orange-100;
color: $white;
max-height: 24px;
position: absolute;
right: -8px;
top: -12.8px;
}
.achievement-icon {
margin: 0 auto;
}
.achievement-list-item {
border-top: 1px solid $gray-500;
padding-bottom: 12px;
padding-top: 11px;
&:last-child {
border-bottom: 1px solid $gray-500;
}
.badge {
background: $gray-600;
color: $gray-300;
height: fit-content;
margin-right: 8px;
}
}
}
</style>
<script>
// import moment from 'moment';
// import axios from 'axios';
// import each from 'lodash/each';
// import cloneDeep from 'lodash/cloneDeep';
// import closeX from '../ui/closeX';
import achievementsLib from '@/../../common/script/libs/achievements';
import Content from '@/../../common/script/content';
import error404 from '../404';
// import { userCustomStateMixin } from '../../mixins/userState';
export default {
components:
error404,
// closeX,
props: ['userId', 'startingPage'],
data () {
return {
selectedPage: 'achievements',
achievements: {},
achievementsCategories: {}, // number, open
content: Content,
};
},
methods: {
async loadUser () {
let user = null;
const profileUserId = this.userId;
if (profileUserId && profileUserId !== this.userLoggedIn._id) {
const response = await this.$store.dispatch('members:fetchMember', {
memberId: profileUserId,
unpack: false,
});
if (response.response && response.response.status === 404) {
user = null;
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('messageDeletedUser'),
type: 'error',
timeout: false,
});
} else if (response.status && response.status === 200) {
user = response.data.data;
}
} else {
user = this.userLoggedIn;
}
if (user) {
if (!user.achievements.quests) user.achievements.quests = {};
if (!user.achievements.challenges) user.achievements.challenges = {};
// @TODO: this common code should handle the above
this.achievements = achievementsLib.getAchievementsForProfile(user);
const achievementsCategories = {};
Object.keys(this.achievements).forEach(category => {
achievementsCategories[category] = {
open: false,
number: Object.keys(this.achievements[category].achievements).length,
};
});
this.achievementsCategories = achievementsCategories;
this.user = user;
}
this.userLoaded = true;
},
},
};
</script>

View file

@ -1,12 +1,16 @@
<template>
<b-modal
id="profile"
size="lg"
:hide-footer="true"
:hide-header="true"
@hide="beforeHide"
@shown="onShown()"
>
<div slot="modal-header">
<close-x
@close="close()"
/>
</div>
<profile
:user-id="userId"
:starting-page="startingPage"
@ -15,33 +19,62 @@
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#profile {
.modal-header {
background-color: $white;
border-bottom: none;
padding: 0px;
}
.modal-dialog {
max-width: 684px;
}
.modal-body {
padding: 0;
border-radius: 12px;
background-color: $white;
}
.modal-content {
background: $gray-700;
padding: 0;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.header {
width: 100%;
.modal-close {
z-index: 1;
}
</style>
<script>
import profile from './profile';
import closeX from '../ui/closeX';
export default {
components: {
profile,
closeX,
},
data () {
return {
userId: undefined,
startingPage: undefined,
path: undefined,
fromPath: undefined,
toPath: undefined,
};
},
mounted () {
this.$root.$on('habitica:show-profile', data => {
this.userId = data.userId;
this.startingPage = data.startingPage || 'profile';
this.path = data.path;
this.fromPath = data.fromPath;
this.toPath = data.toPath;
this.$root.$emit('bv::show::modal', 'profile');
});
},
@ -50,13 +83,16 @@ export default {
},
methods: {
onShown () {
window.history.pushState('', null, this.path);
window.history.pushState('', null, this.toPath);
},
beforeHide () {
if (this.$route.path !== window.location.pathname) {
this.$router.back();
window.history.pushState('', null, this.fromPath);
}
},
close () {
this.$root.$emit('bv::hide::modal', 'profile');
},
},
};

View file

@ -1,35 +0,0 @@
<template>
<div class="container">
<div class="standard-page">
<profile
:user-id="userId"
:starting-page="startingPage"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.header {
width: 100%;
}
</style>
<script>
import profile from './profile';
export default {
components: {
profile,
},
props: {
userId: String,
startingPage: {
type: String,
default: 'profile',
},
},
};
</script>

View file

@ -120,27 +120,27 @@
</h2>
<div class="well pet-mount-well">
<div class="pet-mount-well-image">
<div
class="box"
:class="{white: user.items.currentPet}"
>
<div
class="box"
:class="{white: user.items.currentPet}"
>
<div
class="Pet"
:class="`Pet-${user.items.currentPet}`"
></div>
</div>
class="Pet"
:class="`Pet-${user.items.currentPet}`"
></div>
</div>
<div class="pet-mount-well-text">
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
<div>
<strong>{{ $t('petsFound') }}:</strong>
{{ totalCount(user.items.pets) }}
</div>
<div>
<strong>{{ $t('beastMasterProgress') }}:</strong>
{{ beastMasterProgress(user.items.pets) }}
</div>
</div>
<div class="pet-mount-well-text">
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
<div>
<strong>{{ $t('petsFound') }}:</strong>
{{ totalCount(user.items.pets) }}
</div>
<div>
<strong>{{ $t('beastMasterProgress') }}:</strong>
{{ beastMasterProgress(user.items.pets) }}
</div>
</div>
</div>
</div>
<div class="stats-section-mounts col-12 col-md-6">

View file

@ -1,5 +1,6 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import {
setup as setupAnalytics,
@ -28,6 +29,7 @@ Vue.config.productionTip = IS_PRODUCTION;
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
Vue.use(StoreModule);
Vue.use(BootstrapVue);
Vue.use(Fragment.Plugin);
setUpLogging();
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time

View file

@ -0,0 +1,23 @@
import notifications from './notifications';
export default {
mixins: [notifications],
methods: {
async mixinCopyToClipboard (valueToCopy, notificationToShow = null) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(valueToCopy);
} else {
// fallback if clipboard API does not exist
const copyText = document.createElement('textarea');
copyText.value = valueToCopy;
document.body.appendChild(copyText);
copyText.select();
document.execCommand('copy');
document.body.removeChild(copyText);
}
if (notificationToShow) {
this.text(notificationToShow);
}
},
},
};

View file

@ -10,7 +10,7 @@ function toFixedWithoutRounding (num, fixed) {
return num.toString().match(re)[0];
}
export default {
export const NotificationMixins = {
computed: {
...mapState({ notifications: 'notificationStore' }),
},
@ -90,3 +90,5 @@ export default {
},
},
};
export default NotificationMixins;

View file

@ -0,0 +1,62 @@
/**
* Component Example
*
* <current-password-input
* :show-forget-password="true"
* :is-valid="mixinData.currentPasswordIssues.length === 0"
* :invalid-issues="mixinData.currentPasswordIssues"
* @passwordValue="updates.password = $event"
* />
*/
export const PasswordInputChecksMixin = {
data () {
return {
mixinData: {
currentPasswordIssues: [],
newPasswordIssues: [],
confirmPasswordIssues: [],
},
};
},
methods: {
clearPasswordIssues () {
this.mixinData.currentPasswordIssues.length = 0;
this.mixinData.newPasswordIssues.length = 0;
this.mixinData.confirmPasswordIssues.length = 0;
},
/**
* @param {() => Promise<void>} promiseCall
* @returns {Promise<void>}
*/
async passwordInputCheckMixinTryCall (promiseCall) {
try {
// reset previous issues
this.clearPasswordIssues();
await promiseCall();
} catch (axiosError) {
const message = axiosError.response?.data?.message;
if ([this.$t('wrongPassword'), this.$t('missingPassword')].includes(message)) {
this.mixinData.currentPasswordIssues.push(message);
} else if ([this.$t('missingNewPassword'), this.$t('passwordIssueLength'), this.$t('passwordConfirmationMatch')].includes(message)) {
this.mixinData.newPasswordIssues.push(message);
this.mixinData.confirmPasswordIssues.push(message);
} else if (this.$t('invalidReqParams') === message) {
const errors = axiosError.response?.data?.errors ?? [];
for (const error of errors) {
if (error.param === 'password') {
this.mixinData.currentPasswordIssues.push(error.message);
} else if (error.param === 'newPassword') {
this.mixinData.newPasswordIssues.push(error.message);
} else {
this.mixinData.confirmPasswordIssues.push(error.message);
}
}
}
}
},
},
};

View file

@ -3,7 +3,7 @@ import { mapState } from '@/libs/store';
export const userCustomStateMixin = fieldname => {
const map = { };
map[fieldname] = 'user.data';
return { // eslint-disable-line import/prefer-default-export
return {
computed: {
...mapState(map),
},

View file

@ -0,0 +1,238 @@
<template>
<div class="row">
<secondary-menu class="col-12">
<template
v-for="routePath in tabs"
>
<router-link
v-if="allowedToShowTab(routePath)"
:key="routePath"
class="nav-link"
:to="{name: routePath}"
exact="exact"
:class="{'active': $route.name === routePath}"
>
{{ $t(pathTranslateKey(routePath)) }}
</router-link>
</template>
</secondary-menu>
<div
v-if="$route.name === 'subscription' && promo === 'g1g1'"
class="g1g1-banner d-flex justify-content-center"
@click="showSelectUser"
>
<div
v-once
class="svg-icon svg-gifts left-gift"
v-html="icons.gifts"
>
</div>
<div class="d-flex flex-column align-items-center text-center">
<strong
class="mt-auto mb-1"
> {{ $t('g1g1Event') }} </strong>
<p
class="mb-auto"
>
{{ $t('g1g1Details') }}
</p>
</div>
<div
v-once
class="svg-icon svg-gifts right-gift"
v-html="icons.gifts"
>
</div>
</div>
<div
class="col-12 d-flex "
:class="{'justify-content-center': applyNarrowView}"
>
<div :class="{'settings-content': applyNarrowView, 'full-width-content': !applyNarrowView}">
<router-view />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
strong {
font-size: 1rem;
line-height: 1.25;
}
.g1g1-banner {
color: $white;
width: 100%;
height: 5.75rem;
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
cursor: pointer;
}
.left-gift {
margin: auto 3rem auto auto;
}
.right-gift {
margin: auto auto auto 3rem;
filter: flipH;
transform: scaleX(-1);
}
.svg-gifts {
width: 3.5rem;
}
.full-width-content {
width: 100%;
margin-left: 10%;
margin-right: 10%;
}
.settings-content {
flex: 0 0 732px;
max-width: unset;
::v-deep {
line-height: 1.71;
.small {
line-height: 1.33;
}
table td {
padding: 0.5rem;
}
table tr.expanded td {
padding-bottom: 1.5rem;
}
.settings-label {
font-weight: bold;
color: $gray-50;
width: 23%;
}
.input-area .settings-label {
width: unset;
}
.settings-value {
color: $gray-50;
width: auto;
}
.settings-button {
width: 30%;
text-align: end;
}
.dialog-title {
font-size: 14px;
font-weight: bold;
color: $purple-300;
&.danger {
color: $maroon-50;
}
}
.dialog-disclaimer {
color: $gray-50;
}
.input-area {
width: 320px;
margin: 1rem auto 0;
}
.edit-link {
&:hover {
text-decoration: underline;
}
}
.remove-link {
color: $maroon-50 !important;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<script>
import find from 'lodash/find';
import { mapState } from '@/libs/store';
import SecondaryMenu from '@/components/secondaryMenu';
import gifts from '@/assets/svg/gifts-vertical.svg';
import { userStateMixin } from '@/mixins/userState';
export default {
components: {
SecondaryMenu,
},
mixins: [userStateMixin],
data () {
return {
icons: Object.freeze({
gifts,
}),
tabs: [
'general',
'subscription',
'siteData',
'promoCode',
'transactions',
'notifications',
],
};
},
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
}),
currentEvent () {
return find(this.currentEventList, event => Boolean(event.promo));
},
promo () {
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
return this.currentEvent.promo;
},
applyNarrowView () {
return !['subscription', 'transactions'].includes(this.$route.name);
},
},
methods: {
/**
* @param {String} tabName
* @returns {Boolean}
*/
allowedToShowTab (tabName) {
const transactionsTab = tabName === 'transactions';
return transactionsTab
? this.hasPermission(this.user, 'userSupport')
: true;
},
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');
},
pathTranslateKey (path) {
if (path === 'api') {
return 'API';
}
return path;
},
},
};
</script>

View file

@ -0,0 +1,91 @@
<template>
<div
class="class-value"
:class="{[selectedClass]: !classDisabled, disabled: classDisabled}"
>
<span
v-if="!classDisabled"
class="svg-icon icon-16 mr-2"
v-html="classIcons[selectedClass]"
></span>
<span
v-if="classDisabled"
class="label"
>
{{ $t('noClassSelected') }}
</span>
<span
v-else
class="label"
>
{{ $t(selectedClass) }}
</span>
</div>
</template>
<script>
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';
export default {
name: 'ClassIconLabel',
props: ['selectedClass', 'classDisabled'],
data () {
return {
classIcons: Object.freeze({
warrior: warriorIcon,
rogue: rogueIcon,
healer: healerIcon,
wizard: wizardIcon,
}),
};
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.class-value {
display: flex;
align-items: center;
&:not(.disabled) {
.label {
font-weight: bold;
line-height: 1.71;
}
}
}
.healer {
color: $healer-color;
}
.rogue {
color: $rogue-color;
}
.warrior {
color: $warrior-color;
}
.wizard {
color: $wizard-color;
}
.disabled {
color: $maroon-50;
}
.label {
font-size: 14px;
line-height: 1.71;
text-align: center;
}
</style>

View file

@ -0,0 +1,53 @@
<template>
<div class="input-area">
<validated-text-input
v-model="currentPassword"
:settings-label="customLabel ?? 'password'"
:placeholder="$t(customLabel ?? 'password')"
:is-valid="isValid"
:invalid-issues="invalidIssues"
:only-show-invalid-state="true"
input-type="password"
@update:value="$emit('passwordValue', currentPassword)"
>
<div
v-if="showForgetPassword"
slot="top-right"
class="forgot-password"
>
<router-link
to="/forgot-password"
target="_blank"
>
{{ $t('forgotPassword') }}
</router-link>
</div>
</validated-text-input>
</div>
</template>
<script>
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
export default {
name: 'CurrentPasswordInput',
components: { ValidatedTextInput },
props: ['customLabel', 'showForgetPassword', 'isValid', 'invalidIssues'],
data () {
return {
currentPassword: '',
};
},
};
</script>
<style lang="scss" scoped>
.forgot-password {
a {
font-size: 12px;
line-height: 1.33;
}
}
</style>

View file

@ -0,0 +1,13 @@
export const GenericUserPreferencesMixin = {
methods: {
setUserPreference (preferenceType, subtype) {
const settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
return this.$store.dispatch('user:set', settings);
},
},
};

View file

@ -0,0 +1,83 @@
import { reactive } from 'vue';
export const sharedInlineSettingStore = reactive({
inlineSettingAlreadyOpen: false,
inlineSettingUnsavedValues: false,
/**
* @type InlineSettingMixin
*/
instanceOfCurrentlyOpened: null,
markAsOpened (currentInstance) {
this.inlineSettingAlreadyOpen = true;
this.instanceOfCurrentlyOpened = currentInstance;
},
markAsClosed () {
this.inlineSettingUnsavedValues = false;
this.inlineSettingAlreadyOpen = false;
},
});
export const InlineSettingMixin = {
data () {
return {
mixinData: {
inlineSettingMixin: {
modalVisible: false,
sharedState: sharedInlineSettingStore,
},
},
};
},
methods: {
openModal () {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingAlreadyOpen) {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
if (window.confirm(this.$t('confirmCancelChanges'))) {
this._hidePrevious();
this._openIt();
} else {
return;
}
} else {
this._hidePrevious();
}
}
this._openIt();
},
_openIt () {
this.mixinData.inlineSettingMixin.sharedState.markAsOpened(this);
this.mixinData.inlineSettingMixin.modalVisible = true;
this.$el.scrollTo({
behavior: 'smooth',
});
},
_hidePrevious () {
this.mixinData.inlineSettingMixin.sharedState.instanceOfCurrentlyOpened.resetControls();
this.mixinData.inlineSettingMixin.sharedState.instanceOfCurrentlyOpened.closeModal();
},
/**
* This is just for the cancel buttons - so that they also ask if there are unchanged values
*/
requestCloseModal () {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues && !window.confirm(this.$t('confirmCancelChanges'))) {
return;
}
this.resetControls();
this.closeModal();
},
/**
* This is for the save methods to call it after they are done
*/
closeModal () {
this.mixinData.inlineSettingMixin.modalVisible = false;
this.mixinData.inlineSettingMixin.sharedState.markAsClosed();
},
modalValuesChanged (value = true) {
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = value;
},
resetControls () {},
},
};

View file

@ -0,0 +1,100 @@
<template>
<div class="input-area">
<div class="label-line">
<div class="settings-label">
{{ label }}
</div>
<div
class="link-style"
@click="mixinCopyToClipboard(value, notificationText)"
>
{{ $t('copy') }}
</div>
</div>
<div class="form-group">
<div
class="input-group"
>
<div class="input-group-prepend input-group-icon">
<div
v-once
class="svg-icon icon-16"
v-html="icons.lock"
></div>
</div>
<input
:value="value"
class="form-control"
readonly
aria-readonly="true"
type="text"
>
</div>
</div>
</div>
</template>
<script>
import CopyToClipboard from '@/mixins/copyToClipboard';
import svgLockSmall from '@/assets/svg/lock-small.svg';
export default {
name: 'LockedInput',
mixins: [CopyToClipboard],
props: ['label', 'value', 'notificationText'],
data () {
return {
icons: Object.freeze({
lock: svgLockSmall,
}),
};
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.label-line {
display: flex;
}
.settings-label {
flex: 1;
}
.link-style {
font-size: 12px;
line-height: 1.33;
color: $purple-300;
cursor: pointer;
display: flex;
align-items: center;
&:hover, &:active, &:focus {
text-decoration: underline;
}
}
.input-group {
border-radius: 2px;
input {
border: solid 1px $gray-500;
background-color: $gray-700;
&:hover {
outline: 0;
}
}
}
.input-group-icon {
padding: 8px;
border-radius: 2px;
background-color: $gray-600;
}
</style>

View file

@ -0,0 +1,68 @@
<template>
<div
class="buttons"
:class="{'no-padding': noPadding}"
>
<button
v-if="!hideSave"
class="btn btn-save"
:class="primaryButtonColor ?? 'btn-primary'"
type="submit"
:disabled="disableSave"
@click="$emit('saveClicked')"
>
{{ $t(primaryButtonLabel ?? 'save') }}
</button>
<a
v-if="!hideCancel"
class="edit-link"
@click.prevent="$emit('cancelClicked')"
>
{{ $t('cancel') }}
</a>
</div>
</template>
<script>
export default {
name: 'SaveCancelButtons',
props: {
hideSave: {
type: Boolean,
},
hideCancel: {
type: Boolean,
},
disableSave: {
type: Boolean,
},
noPadding: {
type: Boolean,
},
primaryButtonLabel: {
type: String,
},
primaryButtonColor: {
type: String,
},
},
};
</script>
<style lang="scss" scoped>
.buttons {
align-items: center;
display: flex;
flex-direction: column;
&:not(.no-padding) {
margin-top: 1.5rem;
}
}
.btn-save {
margin-bottom: 1rem;
}
</style>

View file

@ -0,0 +1,56 @@
<template>
<div class="your-balance">
<span
v-once
class="label"
>
{{ $t('yourBalance') }}
</span>
<balance-info
class="balance-info"
:currency-needed="currencyNeeded"
:amount-needed="amountNeeded"
/>
</div>
</template>
<script>
import BalanceInfo from '@/components/shops/balanceInfo.vue';
export default {
name: 'YourBalance',
components: { BalanceInfo },
props: {
currencyNeeded: {
type: String,
},
amountNeeded: {
type: Number,
},
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.your-balance {
padding: 8px 16px;
border-radius: 4px;
background-color: $gray-600;
display: inline-block;
align-self: center;
}
.label {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
}
.balance-info {
display: inline-block !important;
}
</style>

View file

@ -0,0 +1,140 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('generalSettings') }}
</h1>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('account') }}
</h2>
<table class="table">
<user-name-setting />
<user-email-setting />
<display-name-setting />
<password-setting />
<reset-account />
<delete-account />
<tr>
<td colspan="3"></td>
</tr>
</table>
<h2 v-once>
{{ $t('loginMethods') }}
</h2>
<table class="table">
<LoginMethods />
<tr>
<td colspan="3">
</td>
</tr>
</table>
<h2 v-once>
{{ $t('site') }}
</h2>
<table class="table">
<language-setting />
<date-format-setting />
<day-start-adjustment-setting />
<audio-theme-setting />
<sleep-mode />
<tr>
<td colspan="3">
</td>
</tr>
</table>
<h2 v-once>
{{ $t('character') }}
</h2>
<table class="table">
<fix-values-setting />
<class-setting />
<tr>
<td colspan="3">
</td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.standard-page {
padding-left: 0;
padding-right: 0;
}
.table {
color: $gray-50;
}
</style>
<script>
import notificationsMixin from '../../mixins/notifications';
import UserNameSetting from './settingRows/userNameSetting';
import UserEmailSetting from './settingRows/userEmailSetting';
import DisplayNameSetting from './settingRows/displayNameSetting';
import PasswordSetting from './settingRows/passwordSetting';
import ResetAccount from './settingRows/resetAccount';
import DeleteAccount from './settingRows/deleteAccount';
import { sharedInlineSettingStore } from './components/inlineSettingMixin';
import LanguageSetting from './settingRows/languageSetting';
import DateFormatSetting from './settingRows/dateFormatSetting';
import DayStartAdjustmentSetting from './settingRows/dayStartAdjustmentSetting.vue';
import AudioThemeSetting from '@/pages/settings/settingRows/audioThemeSetting.vue';
import ClassSetting from '@/pages/settings/settingRows/classSetting.vue';
import FixValuesSetting from '@/pages/settings/settingRows/fixValuesSetting.vue';
import LoginMethods from '@/pages/settings/settingRows/loginMethods.vue';
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
import { mapState } from '@/libs/store';
import SleepMode from '@/pages/settings/settingRows/sleepMode.vue';
export default {
components: {
SleepMode,
LoginMethods,
FixValuesSetting,
ClassSetting,
AudioThemeSetting,
DayStartAdjustmentSetting,
DateFormatSetting,
LanguageSetting,
DeleteAccount,
ResetAccount,
PasswordSetting,
DisplayNameSetting,
UserEmailSetting,
UserNameSetting,
},
mixins: [notificationsMixin, GenericUserPreferencesMixin],
computed: {
...mapState({
user: 'user.data',
}),
},
beforeRouteLeave (_, __, next) {
sharedInlineSettingStore.markAsClosed();
next();
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('generalSettings'),
});
},
};
</script>

View file

@ -0,0 +1,325 @@
<template>
<div class="row standard-page px-0">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('notifications') }}
</h1>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('allNotifications') }}
</h2>
<table class="table">
<tr>
<td class="bold">
{{ $t('unsubscribeAllPush') }}
</td>
<td>
<toggle-switch
:checked="user.preferences.pushNotifications.unsubscribeFromAll"
@change="set('pushNotifications', 'unsubscribeFromAll', $event)"
/>
</td>
</tr>
<tr>
<td>
<span class="bold">{{ $t('unsubscribeAllEmails') }}</span> <br>
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
</td>
<td>
<toggle-switch
:checked="user.preferences.emailNotifications.unsubscribeFromAll"
@change="set('emailNotifications', 'unsubscribeFromAll', $event)"
/>
</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
</div>
<div class="col-12">
<h2>Website</h2>
<table class="table">
<tr>
<td
v-once
class="bold"
>
{{ $t('showLevelUpModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.levelUp"
class="toggle-switch-width"
@change="set('suppressModals', 'levelUp', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showHatchPetModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.hatchPet"
class="toggle-switch-width"
@change="set('suppressModals', 'hatchPet', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showRaisePetModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.raisePet"
class="toggle-switch-width"
@change="set('suppressModals', 'raisePet', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showStreakModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.streak"
class="toggle-switch-width"
@change="set('suppressModals', 'streak', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('baileyAnnouncement') }}
</td>
<td class="email_push_col show_bailey_col">
<b-popover
target="viewBaileyLink"
triggers="hover"
placement="right"
:content="$t('showBaileyPop')"
/>
<a
id="viewBaileyLink"
class="show_bailey_link"
@click="showBailey()"
>
{{ $t('view') }}
</a>
</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
</div>
<div class="col-12">
<h2>Email & Push</h2>
<table class="table">
<tr>
<td></td>
<th class="email_push_col email_col_padding">
<span v-once>{{ $t('email') }}</span>
</th>
<th class="email_push_col">
<span v-once>{{ $t('push') }}</span>
</th>
</tr>
<tr
v-for="notification in notificationsIds"
:key="notification"
>
<td
v-once
class="bold"
>
{{ $t(notification) }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="user.preferences.emailNotifications[notification]"
class="toggle-switch-width"
@change="set('emailNotifications', notification, $event)"
/>
</td>
<td class="email_push_col">
<toggle-switch
v-if="!onlyEmailsIds.includes(notification)"
:checked="user.preferences.pushNotifications[notification]"
class="toggle-switch-width"
@change="set('pushNotifications', notification, $event)"
/>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.toggle-switch-width {
::v-deep {
.toggle-switch {
margin-left: 0;
}
}
}
.email_push_col {
width: 50px;
padding-left: 0 !important;
padding-right: 0 !important;
}
/** Table Styles, maybe can be copied / extracted once more Pages need it */
.table {
margin-bottom: 0.5rem;
}
.table th, .table td {
padding: 0.5rem;
}
.bold {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
small {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
}
.email_col_padding {
padding-right: 70px !important;
}
toggle-switch {
padding-right: 8px;
}
.show_bailey_col {
text-align: right;
}
.show_bailey_link {
padding-right: 8px;
line-height: 1.71;
// color: $blue-10 !important;
&:hover {
text-decoration: underline;
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import notificationsMixin from '@/mixins/notifications';
import ToggleSwitch from '@/components/ui/toggleSwitch';
export default {
components: { ToggleSwitch },
mixins: [notificationsMixin],
data () {
return {
notificationsIds: Object.freeze([
'majorUpdates',
'newPM',
'giftedGems',
'giftedSubscription',
'invitedParty',
'invitedGuild',
'invitedQuest',
'questStarted',
'wonChallenge',
// 'weeklyRecaps',
'kickedGroup',
'onboarding',
'importantAnnouncements',
'subscriptionReminders',
]),
// list of email-only notifications
onlyEmailsIds: Object.freeze([
'kickedGroup',
'importantAnnouncements',
'weeklyRecaps',
'onboarding',
'subscriptionReminders',
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('notifications'),
});
// If ?unsubFrom param is passed with valid email type,
// automatically unsubscribe users from that email and
// show an alert
// A simple object to map the key stored in the db (user.preferences.emailNotification[key])
// to its string id but ONLY when the preferences' key and the string key don't match
const MAP_PREF_TO_EMAIL_STRING = {
importantAnnouncements: 'inactivityEmails',
};
const { unsubFrom } = this.$route.query;
if (unsubFrom) {
await this.$store.dispatch('user:set', {
[`preferences.emailNotifications.${unsubFrom}`]: false,
});
const emailTypeString = this.$t(MAP_PREF_TO_EMAIL_STRING[unsubFrom] || unsubFrom);
this.text(this.$t('correctlyUnsubscribedEmailType', { emailType: emailTypeString }));
}
},
methods: {
set (preferenceType, notification, $event) {
const settings = {};
settings[`preferences.${preferenceType}.${notification}`] = $event ?? this.user.preferences[preferenceType][notification];
this.$store.dispatch('user:set', settings);
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
},
};
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('promoCode') }}
</h1>
<div class="input-area">
<div
class="form-inline"
role="form"
>
<input
v-model="couponCode"
class="form-control w-100"
type="text"
:placeholder="$t('promoPlaceholder')"
>
</div>
<div
v-once
class="small mt-2"
>
{{ $t('couponText') }}
</div>
<save-cancel-buttons
:hide-cancel="true"
primary-button-label="submit"
@saveClicked="enterCoupon()"
/>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import SaveCancelButtons from '@/pages/settings/components/saveCancelButtons.vue';
export default {
components: { SaveCancelButtons },
mixins: [notifications],
data () {
return {
codes: {
event: '',
count: '',
},
couponCode: '',
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('promoCode'),
});
},
methods: {
async enterCoupon () {
const code = await axios.post(`/api/v4/coupons/enter/${this.couponCode}`);
if (!code) return;
this.$store.state.user.data = code.data.data;
this.text(this.$t('promoCodeApplied'));
},
},
};
</script>

View file

@ -9,7 +9,7 @@
<script>
import { mapState } from '@/libs/store';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import PurchaseHistoryTable from '../../components/ui/purchaseHistoryTable.vue';
export default {
components: {

View file

@ -0,0 +1,197 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td
v-once
class="settings-label"
>
{{ $t("audioTheme") }}
</td>
<td class="settings-value">
{{ $t(`audioTheme_${currentAudioTheme}`) }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("audioTheme") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("audioThemeDisclaimer") }}</span>
</div>
<div class="input-area">
<div class="label-columns">
<div class="settings-label">
{{ $t("enableAudio") }}
</div>
<div>
<toggle-switch
:checked="!isDisabled"
@change="toggleAudioThemeOff($event)"
/>
</div>
</div>
<div class="label-columns mb-2">
<div class="settings-label">
{{ $t("audioTheme") }}
</div>
<div v-if="!isDisabled">
<a
class="edit-link"
@click.prevent="playAudio()"
>
{{ $t('playDemoAudio') }}
</a>
</div>
</div>
<div class="form-group">
<select-list
:disabled="isDisabled"
:items="availableAudioThemes"
:value="themeSelected"
@select="changeAudioThemeTemporary($event)"
>
<template #item="{ item, button }">
<span v-if="button">
{{ $t(`audioTheme_${themeSelected}`) }}
</span>
<span v-else>
{{ $t(`audioTheme_${item}`) }}
</span>
</template>
</select-list>
</div>
</div>
<save-cancel-buttons
:disable-save="previousValue === currentAudioTheme"
@saveClicked="changeAudioThemeAndClose()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.label-columns {
display: flex;
&:first-of-type {
margin-bottom: 1rem;
}
div:first-of-type {
flex: 1
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SelectList from '@/components/ui/selectList';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
import sounds from '@/libs/sounds';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
export default {
components: { ToggleSwitch, SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
soundIndex: 0,
previousValue: '',
// using the user.preferences didn't update the select-list values from off state
themeSelected: '',
};
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
availableAudioThemes () {
return this.content.audioThemes;
},
currentAudioTheme () {
return this.user.preferences.sound;
},
isDisabled () {
return this.currentAudioTheme === 'off';
},
},
mounted () {
this.previousValue = this.currentAudioTheme;
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.changeAudioThemeTemporary(this.previousValue);
},
changeAudioThemeTemporary ($event) {
this.user.preferences.sound = $event;
this.themeSelected = $event;
this.soundIndex = 0;
},
changeAudioThemeAndClose () {
this.setUserPreference('sound');
this.previousValue = this.user.preferences.sound;
this.closeModal();
},
playAudio () {
this.$root.$emit('playSound', sounds[this.soundIndex]);
this.soundIndex = (this.soundIndex + 1) % sounds.length;
},
toggleAudioThemeOff (enabled) {
if (enabled) {
const [audioTheme] = this.availableAudioThemes;
this.changeAudioThemeTemporary(audioTheme);
} else {
this.changeAudioThemeTemporary('off');
}
this.modalValuesChanged();
},
},
};
</script>

View file

@ -0,0 +1,252 @@
<template>
<fragment v-if="allowedToChangeClass">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("changeClassSetting") }}
</td>
<td class="settings-value">
<class-icon-label
:selected-class="selectedClass"
:class-disabled="classDisabled"
/>
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="showRealModalOrInline()"
>
{{ $t(classDisabled ? 'chooseClassSetting' : 'edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("changeClassSetting") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("changeClassDisclaimer") }}</span>
</div>
<div class="content-centered">
<div class="current-class mt-3">
<span class="label">{{ $t('currentClass') }}:</span>
<class-icon-label
:selected-class="selectedClass"
:class-disabled="classDisabled"
/>
</div>
<gem-price
gem-price="3"
icon-size="24"
class="gem-price-spacing"
:with-background="true"
/>
<save-cancel-buttons
primary-button-label="changeClassSetting"
class="mb-2"
:no-padding="true"
:disable-save="!enoughGemsAvailable"
@saveClicked="changeClassAndClose()"
@cancelClicked="requestCloseModal()"
/>
<your-balance
:amount-needed="amountNeeded"
currency-needed="gems"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.label-columns {
display: flex;
&:first-of-type {
margin-bottom: 1rem;
}
div:first-of-type {
flex: 1
}
}
.content-centered {
display: flex;
flex-direction: column;
}
.gem-price-spacing {
margin-top: 1.5rem;
margin-bottom: 1.25rem;
justify-content: center;
}
.class-selection {
display: flex;
gap: 22px;
justify-content: center;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
}
.label {
font-size: 14px;
line-height: 1.71;
text-align: center;
}
.selected-badge {
position: absolute;
bottom: -1rem;
width: 24px;
height: 24px;
padding: 4px;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
background-color: $green-50;
border-radius: 1rem;
color: $white;
}
.current-class {
display: flex;
justify-content: center;
.label {
margin-right: 0.5rem;
font-weight: bold;
color: $gray-50;
}
}
</style>
<script>
import axios from 'axios';
import { mapGetters, mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
import YourBalance from '@/pages/settings/components/yourBalance.vue';
import GemPrice from '@/components/shops/gemPrice.vue';
import checkIcon from '@/assets/svg/check.svg';
import changeClass from '@/../../common/script/ops/changeClass';
import ClassIconLabel from '@/pages/settings/components/classIconLabel.vue';
export default {
components: {
ClassIconLabel,
GemPrice,
YourBalance,
SaveCancelButtons,
},
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
amountNeeded: 3 / 4,
selectedClass: '',
icons: Object.freeze({
check: checkIcon,
}),
};
},
computed: {
...mapGetters({
userGems: 'user:gems',
}),
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
classList () {
return this.content.classes;
},
allowedToChangeClass () {
return this.user.stats.lvl >= 10;
},
enoughGemsAvailable () {
return this.amountNeeded <= this.userGems;
},
classDisabled () {
return this.user.preferences.disableClasses;
},
},
mounted () {
this.selectedClass = this.user.stats.class;
this.resetControls();
},
methods: {
showRealModalOrInline () {
if (!this.classDisabled) {
this.openModal();
} else {
this.changeClassAndClose();
}
},
async changeClassAndClose () {
if (!this.classDisabled && !window.confirm(this.$t('changeClassConfirmCost'))) {
return;
}
this.$root.$once('bv::hide::modal', () => {
// update the label in the settings list
this.selectedClass = this.user.stats.class;
});
try {
await Promise.all([
// resets the class settings and triggers indirectly the modal of
// src/components/achievemnts/chooseClass - I don't know if we should keep this weird way
changeClass(this.user),
axios.post('/api/v4/user/change-class'),
]);
} catch (e) {
window.alert(e.message); // eslint-disable-line no-alert
}
this.closeModal();
},
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.selectedClass = this.user.stats.class;
},
},
};
</script>

View file

@ -0,0 +1,121 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("dateFormat") }}
</td>
<td class="settings-value">
{{ currentActiveFormat }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("dateFormat") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("dateFormatDisclaimer") }}</span>
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("dateFormat") }}
</div>
<div class="form-group">
<select-list
:items="availableFormats"
:value="selectedFormat"
@select="changeFormat($event)"
/>
</div>
</div>
<save-cancel-buttons
:disable-save="selectedFormat === currentActiveFormat"
@saveClicked="changeFormatAndClose()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
</style>
<script>
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SelectList from '@/components/ui/selectList';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
export default {
components: { SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
selectedFormat: '',
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
};
},
computed: {
...mapState({
user: 'user.data',
}),
currentActiveFormat () {
return this.user.preferences.dateFormat;
},
},
mounted () {
this.resetControls();
},
methods: {
changeFormat (e) {
this.selectedFormat = e;
this.modalValuesChanged();
},
async changeFormatAndClose () {
this.user.preferences.dateFormat = this.selectedFormat;
await this.setUserPreference('dateFormat');
this.closeModal();
},
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.selectedFormat = this.currentActiveFormat;
},
},
};
</script>

View file

@ -0,0 +1,181 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("dayStartAdjustment") }}
</td>
<td class="settings-value">
{{ selectedDayStartLabel(user.preferences.dayStart) }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("dayStartAdjustment") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="$t('customDayStartInfo1')"
>
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("adjustment") }}
</div>
<div class="form-group">
<select-list
:items="dayStartOptions"
:value="newDayStart"
key-prop="value"
active-key-prop="value"
:hide-icon="false"
@select="changeDayStart($event)"
>
<template #item="{ item }">
<span v-if="item === newDayStart || (!item && newDayStart === 0)">
{{ selectedDayStartLabel(newDayStart) }}
</span>
<span v-else>
{{ item?.name }}
</span>
</template>
</select-list>
</div>
</div>
<small
class="timezone-explain"
>
<p v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})"></p>
<p v-html="$t('timezoneInfo')"></p>
</small>
<div class="input-area">
<save-cancel-buttons
:disable-save="newDayStart === user.preferences.dayStart"
@saveClicked="saveDayStart()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.timezone-explain {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
text-align: center;
}
</style>
<script>
import axios from 'axios';
import moment from 'moment/moment';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import SelectList from '@/components/ui/selectList.vue';
import getUtcOffset from '../../../../../common/script/fns/getUtcOffset';
export default {
components: { SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
const dayStartOptions = [];
for (let number = 0; number <= 12; number += 1) {
const meridian = number < 12 ? 'AM' : 'PM';
const hour = number % 12;
const timeWithMeridian = `(${hour || 12}:00 ${meridian})`;
const option = {
value: number,
name: `+${number} hours ${timeWithMeridian}`,
};
if (number === 0) {
option.name = `Default ${timeWithMeridian}`;
}
dayStartOptions.push(option);
}
return {
newDayStart: 0,
dayStartOptions,
};
},
mounted () {
this.newDayStart = this.user.preferences.dayStart;
},
computed: {
...mapState({
user: 'user.data',
}),
timezoneOffsetToUtc () {
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
return `UTC${offsetString}`;
},
dayStart () {
return this.user.preferences.dayStart;
},
},
methods: {
changeDayStart ($event) {
this.newDayStart = $event.value;
},
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v4/user/custom-day-start', {
dayStart: this.newDayStart,
});
this.closeModal();
},
selectedDayStartLabel (dayStartValue) {
if (!this.dayStartOptions) {
return '';
}
return this.dayStartOptions.find(l => l.value === dayStartValue)?.name ?? '';
},
calculateNextCron () {
let nextCron = moment()
.hours(this.newDayStart)
.minutes(0)
.seconds(0)
.milliseconds(0);
const currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
},
},
};
</script>

View file

@ -0,0 +1,153 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("deleteAccount") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title danger"
>
{{ $t("deleteAccount") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="hasPassword
? $t('deleteLocalAccountText')
: $t('deleteSocialAccountText', {magicWord: 'DELETE'})"
>
</div>
<current-password-input
v-if="hasPassword"
:show-forget-password="true"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="passwordValue = $event"
/>
<div
v-else
class="input-area"
>
<div
class="form"
>
<div class="settings-label">
{{ $t("confirm") }}
</div>
<div class="form-group">
<input
v-model="passwordValue"
class="form-control"
type="text"
>
</div>
</div>
</div>
<div
v-once
class="feedback"
v-html="$t('feedback')"
>
</div>
<div
class="input-area"
>
<textarea
id="feedbackTextArea"
v-model="feedback"
:placeholder="$t('feedbackPlaceholder')"
class="form-control"
></textarea>
</div>
<div class="input-area">
<save-cancel-buttons
:disable-save="!enableDelete"
primary-button-color="btn-danger"
primary-button-label="deleteAccount"
@saveClicked="deleteAccount()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.feedback {
color: $gray-50;
}
</style>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import CurrentPasswordInput from '../components/currentPasswordInput.vue';
import { PasswordInputChecksMixin } from '@/mixins/passwordInputChecks';
export default {
components: { CurrentPasswordInput, SaveCancelButtons },
mixins: [InlineSettingMixin, PasswordInputChecksMixin],
data () {
return {
passwordValue: '',
feedback: '',
};
},
computed: {
...mapState({
user: 'user.data',
}),
hasPassword () {
return this.user.auth.local.has_password;
},
enableDelete () {
return this.hasPassword ? Boolean(this.passwordValue) : this.passwordValue === 'DELETE';
},
},
methods: {
async deleteAccount () {
await this.passwordInputCheckMixinTryCall(async () => {
await axios.delete('/api/v4/user', {
data: {
password: this.passwordValue,
feedback: this.feedback,
},
});
localStorage.clear();
window.location.href = '/static/home';
});
},
},
};
</script>

View file

@ -0,0 +1,229 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("displayName") }}
</td>
<td class="settings-value">
{{ user.profile.name }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("displayName") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("changeDisplayNameDisclaimer") }}
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("displayName") }}
</div>
<div
class="form"
name="changeDisplayName"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeDisplayname"
v-model="temporaryDisplayName"
class="form-control"
type="text"
:placeholder="$t('newDisplayName')"
:class="{'is-invalid input-invalid': displayNameInvalid}"
@keyup="valuesChanged()"
>
<div
v-if="displayNameIssues.length > 0"
class="mb-3"
>
<div
v-for="issue in displayNameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
</div>
</div>
</div>
<save-cancel-buttons
:disable-save="displayNameCannotSubmit"
@saveClicked="changeDisplayName(temporaryDisplayName)"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group {
position: relative;
background: white;
}
input {
margin-right: 2rem;
}
.input-floating-checkmark {
position: absolute;
background: none !important;
right: 0.5rem;
top: 0.5rem;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.input-group.is-valid {
border-color: $green-10 !important;
}
.input-group:not(.is-valid) {
.check-icon {
display: none;
}
}
.check-icon {
width: 12px;
height: 10px;
color: $green-50;
}
.form-group {
margin-bottom: 1.5rem;
}
</style>
<script>
import axios from 'axios';
import * as validator from 'validator';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
import checkIcon from '@/assets/svg/check.svg';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import NotificationMixins from '@/mixins/notifications';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin, NotificationMixins],
data () {
return {
temporaryDisplayName: '',
inputChanged: false,
displayNameIssues: [],
updates: {
newEmail: '',
password: '',
},
icons: Object.freeze({
checkIcon,
}),
};
},
computed: {
...mapState({
user: 'user.data',
}),
validEmail () {
return validator.isEmail(this.updates.newEmail);
},
allowedToSave () {
return !this.validEmail || this.updates.password.length === 0;
},
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) {
return true;
}
return this.displayNameIssues.length !== 0;
},
displayNameCannotSubmit () {
return this.displayNameInvalid || !this.inputChanged;
},
},
watch: {
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
},
mounted () {
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.temporaryDisplayName = this.user.profile.name;
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', { 'profile.name': newName });
this.text(this.$t('displayNameSuccess'));
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
this.closeModal();
},
validateDisplayName: debounce(async function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
const res = await this.$store.dispatch('auth:verifyDisplayName', {
displayName,
});
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
}, 500),
valuesChanged () {
this.inputChanged = true;
this.modalValuesChanged();
},
},
};
</script>

View file

@ -0,0 +1,291 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("fixValues") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td
colspan="3"
novalidate="novalidate"
>
<div
v-once
class="dialog-title"
>
{{ $t("fixValues") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span v-html="$t('fixValuesText1')"></span>
<br>
<br>
<span v-html="$t('fixValuesText2')"></span>
</div>
<div class="content-centered">
<div class="input-rows row">
<div
v-for="input in inputList"
:key="input.property"
class="col-4"
>
<div class="fix-value-group mt-3">
<span class="fix-label">
{{ $t(input.translationKey) }}
</span>
<div class="input-group">
<div class="input-group-prepend positive-addon input-group-icon">
<div
v-once
class="svg-icon icon-16"
:class="{[input.translationKey]: true}"
v-html="input.icon"
></div>
</div>
<input
v-model="restoreValues[input.property]"
class="form-control"
type="number"
min="0"
required="required"
@keydown="markAsChanged(input, $event)"
>
</div>
</div>
</div>
</div>
</div>
<save-cancel-buttons
:disable-save="!mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues"
class="mt-4"
@saveClicked="save()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group {
position: relative;
background: white;
}
.input-rows {
width: calc(600px + 1.5rem);
}
.content-centered {
display: flex;
flex-direction: column;
align-items: center;
}
.fix-label {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
.svg-icon.icon-16 {
width: 16px !important;
height: 16px !important;
display: flex;
}
input[type="number"] {
-moz-appearance: textfield !important;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.svg-icon.level {
color: $gray-200;
:global svg path {
fill: currentColor;
}
}
</style>
<script>
// import clone from 'lodash/clone';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import healthIcon from '@/assets/svg/health.svg';
import experienceIcon from '@/assets/svg/experience.svg';
import manaIcon from '@/assets/svg/mana.svg';
import svgGold from '@/assets/svg/gold.svg';
import level from '@/assets/svg/level.svg';
import streakIcon from '@/assets/svg/streak.svg';
import { mapState } from '@/libs/store';
import { MAX_LEVEL_HARD_CAP } from '../../../../../common/script/constants';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
return {
restoreValues: {
hp: 0,
mp: 0,
gp: 0,
exp: 0,
lvl: 0,
streak: 0,
},
icons: Object.freeze({
health: healthIcon,
experience: experienceIcon,
mana: manaIcon,
gold: svgGold,
level,
streak: streakIcon,
}),
inputList: Object.freeze([
{
translationKey: 'health',
icon: healthIcon,
property: 'hp',
},
{
translationKey: 'experience',
icon: experienceIcon,
property: 'exp',
}, {
translationKey: 'mana',
icon: manaIcon,
property: 'mp',
}, {
translationKey: 'gold',
icon: svgGold,
property: 'gp',
},
{
translationKey: 'level',
icon: level,
property: 'lvl',
},
{
translationKey: 'fix21Streaks',
icon: streakIcon,
property: 'streak',
},
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.resetControls();
},
methods: {
resetControls () {
const {
hp, mp, gp, exp, lvl,
} = this.user.stats;
this.restoreValues = {
hp, mp, gp, exp, lvl, streak: this.user.achievements.streak,
};
},
close () {
this.validateInputs();
},
markAsChanged (inputType, keyupEvent) {
this.restoreValues[inputType.property] = keyupEvent.target.value;
this.modalValuesChanged();
},
save () {
if (!this.validateInputs()) {
return;
}
if (this.restoreValues.lvl > MAX_LEVEL_HARD_CAP) {
this.restoreValues.lvl = MAX_LEVEL_HARD_CAP;
}
const userChangedLevel = this.restoreValues.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) {
this.restoreValues.exp = 0;
}
const settings = {
'stats.hp': Number(this.restoreValues.hp),
'stats.exp': Number(this.restoreValues.exp),
'stats.gp': Number(this.restoreValues.gp),
'stats.lvl': Number(this.restoreValues.lvl),
'stats.mp': Number(this.restoreValues.mp),
'achievements.streak': Number(this.restoreValues.streak),
};
this.$store.dispatch('user:set', settings);
this.wasChanged = false;
this.closeModal();
},
validateInputs () {
const canRestore = ['hp', 'exp', 'gp', 'mp'];
let valid = true;
for (const stat of canRestore) {
if (this.restoreValues[stat] === ''
|| this.restoreValues[stat] < 0
) {
this.restoreValues[stat] = this.user.stats[stat];
valid = false;
}
}
const inputLevel = Number(this.restoreValues.lvl);
if (this.restoreValues.lvl === ''
|| !Number.isInteger(inputLevel)
|| inputLevel < 1) {
this.restoreValues.lvl = this.user.stats.lvl;
valid = false;
}
const inputStreak = Number(this.restoreValues.streak);
if (this.restoreValues.streak === ''
|| !Number.isInteger(inputStreak)
|| inputStreak < 0) {
this.restoreValues.streak = this.user.achievements.streak;
valid = false;
}
return valid;
},
},
};
</script>

View file

@ -0,0 +1,62 @@
<template>
<fragment>
<tr>
<td class="settings-label">
{{ $t("showHeader") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<toggle-switch
v-model="user.preferences.showHeader"
@change="setUserPreference('showHeader')"
/>
</td>
</tr>
<tr>
<td class="settings-label">
{{ $t("stickyHeader") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<toggle-switch
v-model="user.preferences.stickyHeader"
@change="setUserPreference('stickyHeader')"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
::v-deep {
.toggle-switch-outer {
display: inline-block;
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
export default {
components: { ToggleSwitch },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
},
};
</script>

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