mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-20 03:14:01 -05:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b56a4689e | |||
| 966bfde06f | |||
| 9d23693a45 | |||
| b57fb94579 | |||
| 42805a2792 | |||
| d7e7668255 | |||
| a4fda59a69 | |||
| 3b9ffae625 | |||
| dd413518f5 | |||
| c581b88213 | |||
| 0078d8f2b2 | |||
| 54ea0aab18 | |||
| 24b2a5beb8 | |||
| fe9332dff4 | |||
| c6582e4c3c | |||
| b4b7980eee | |||
| 6069fbd61f | |||
| 38a591bdd1 | |||
| 2736d8acf3 | |||
| 8fe13dbb23 | |||
| 4581bb9315 | |||
| 2999212379 | |||
| d2bd246e6e | |||
| 1482f6c225 | |||
| 1178da3a26 | |||
| 819ed2b355 | |||
| a92999fc11 | |||
| 3489b88752 | |||
| 94bda30385 | |||
| e8bbdc2cb8 | |||
| d465efaf96 | |||
| 3a08de7ab3 | |||
| e6ffd69148 | |||
| 746fcfff49 | |||
| 8aa343d390 | |||
| d80c43c82a | |||
| 80e4b8617a | |||
| b3fac011a9 | |||
| 8f4d871911 | |||
| 19373ce84d | |||
| 55bfca20d9 | |||
| 2ee2b05d1c | |||
| 3d3db1bdd9 | |||
| a5dff99fa1 | |||
| 25435218e1 | |||
| 7746049bb4 | |||
| 036bf43cf3 | |||
| c4e0127a37 | |||
| 7ae059e243 | |||
| 81b9a0b92d | |||
| a6e87452a6 | |||
| c40d384913 | |||
| 65144aef28 |
@@ -21,3 +21,4 @@ services:
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
+1
-1
Submodule habitica-images updated: 32a4678c6b...b7367f328a
Generated
+290
-547
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.46.4",
|
||||
"version": "5.48.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,7 +19,6 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bullmq": "^5.71.1",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -31,6 +30,7 @@
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.21.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-sitemap-xml": "^3.1.0",
|
||||
"express-validator": "^5.2.0",
|
||||
"firebase-admin": "^12.1.1",
|
||||
"glob": "^8.1.0",
|
||||
@@ -44,7 +44,6 @@
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -54,7 +53,7 @@
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"mongoose": "^8.23.0",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
@@ -68,6 +67,7 @@
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
@@ -77,7 +77,6 @@
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
|
||||
@@ -1,560 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#getServiceByEnvironment', () => {
|
||||
it('returns mock methods when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment())
|
||||
.to.equal(analyticsService.mockAnalyticsService);
|
||||
});
|
||||
|
||||
it('returns real methods when in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
|
||||
.to.equal(analyticsService.track);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
|
||||
.to.equal(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType; let
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', () => {
|
||||
data.itemKey = 'headAccessory_special_foxEars';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Fox Ears',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', () => {
|
||||
data.itemKey = 'Wolf';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Wolf Egg',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', () => {
|
||||
data.itemKey = 'Cake_Skeleton';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Bare Bones Cake',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', () => {
|
||||
data.itemKey = 'Golden';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Golden Hatching Potion',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', () => {
|
||||
data.itemKey = 'atom1';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', () => {
|
||||
data.itemKey = 'seafoam';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Seafoam',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
paymentMethod: 'PayPal',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1,
|
||||
sku: 'paypal-checkout',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
it('has stubbed track method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
|
||||
});
|
||||
|
||||
it('has stubbed trackPurchase method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
|
||||
});
|
||||
});
|
||||
});
|
||||
+116
-136
@@ -13,7 +13,6 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
@@ -41,20 +40,17 @@ describe('cron', async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.spy(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (clock !== null) clock.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
@@ -63,7 +59,7 @@ describe('cron', async () => {
|
||||
it('resets user.items.lastDrop.count', async () => {
|
||||
user.items.lastDrop.count = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
});
|
||||
@@ -71,26 +67,11 @@ describe('cron', async () => {
|
||||
it('increments user cron count', async () => {
|
||||
const cronCountBefore = user.flags.cronCount;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||
});
|
||||
|
||||
it('calls analytics', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', async () => {
|
||||
beforeEach(async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -101,7 +82,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -112,7 +93,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -122,7 +103,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -131,7 +112,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
user.purchased.plan.dateUpdated = undefined;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -142,7 +123,7 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
@@ -150,7 +131,7 @@ describe('cron', async () => {
|
||||
it('resets plan.dateUpdated on a new month', async () => {
|
||||
const currentMonth = moment().startOf('month');
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
@@ -158,7 +139,7 @@ describe('cron', async () => {
|
||||
it('increments plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
@@ -166,7 +147,7 @@ describe('cron', async () => {
|
||||
it('increments plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
@@ -175,7 +156,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
@@ -184,7 +165,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
||||
});
|
||||
@@ -196,7 +177,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
@@ -206,7 +187,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -214,7 +195,7 @@ describe('cron', async () => {
|
||||
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
@@ -225,7 +206,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
@@ -264,7 +245,7 @@ describe('cron', async () => {
|
||||
// Add 2 days so that we're sure we're not affected by any start-of-month effects
|
||||
// e.g., from time zone oddness.
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
user: user1, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -276,7 +257,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
user: user1, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -311,7 +292,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
user: user3, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -323,7 +304,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
user: user3, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -358,7 +339,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
user: user6, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user6.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -391,7 +372,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
user: user12, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -403,7 +384,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
user: user12, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -439,7 +420,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
@@ -452,7 +433,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
});
|
||||
// subscription has been erased by now
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
||||
@@ -471,7 +452,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -482,14 +463,14 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
||||
});
|
||||
@@ -497,7 +478,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||
});
|
||||
@@ -505,7 +486,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
||||
});
|
||||
@@ -513,7 +494,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
||||
});
|
||||
@@ -521,7 +502,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
});
|
||||
@@ -530,7 +511,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -538,7 +519,7 @@ describe('cron', async () => {
|
||||
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
});
|
||||
@@ -564,7 +545,7 @@ describe('cron', async () => {
|
||||
it('should make uncompleted todos redder', async () => {
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||
});
|
||||
@@ -573,7 +554,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
||||
});
|
||||
@@ -582,7 +563,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
@@ -608,7 +589,7 @@ describe('cron', async () => {
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
@@ -635,7 +616,7 @@ describe('cron', async () => {
|
||||
const original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
let listsAreEqual = true;
|
||||
@@ -675,7 +656,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
@@ -686,7 +667,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
@@ -696,14 +677,14 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
});
|
||||
@@ -711,7 +692,7 @@ describe('cron', async () => {
|
||||
it('should set tasks completed to false', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -720,7 +701,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -729,7 +710,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -739,7 +720,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -749,7 +730,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -759,7 +740,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
@@ -770,7 +751,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
@@ -784,7 +765,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -797,7 +778,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -808,7 +789,7 @@ describe('cron', async () => {
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -816,7 +797,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -829,7 +810,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(-1);
|
||||
@@ -841,7 +822,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
@@ -862,7 +843,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[1].frequency = 'daily';
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(48);
|
||||
@@ -886,7 +867,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -897,7 +878,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].up = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -909,7 +890,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.equal(1);
|
||||
@@ -928,7 +909,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -941,7 +922,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -955,7 +936,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -964,7 +945,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 8;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -988,7 +969,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1002,7 +983,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset after user CDS
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1026,7 +1007,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1036,7 +1017,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1060,7 +1041,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1084,7 +1065,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1098,7 +1079,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1107,7 +1088,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 32;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1132,7 +1113,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1156,7 +1137,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1166,7 +1147,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1199,7 +1180,7 @@ describe('cron', async () => {
|
||||
user.stats.lvl = 2;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.history.exp).to.have.lengthOf(1);
|
||||
@@ -1212,7 +1193,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(1);
|
||||
@@ -1224,7 +1205,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
@@ -1238,7 +1219,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1256,7 +1237,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1280,7 +1261,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1307,7 +1288,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1333,7 +1314,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1360,7 +1341,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1381,7 +1362,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1401,7 +1382,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1420,7 +1401,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
||||
|
||||
@@ -1436,7 +1417,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
@@ -1449,7 +1430,7 @@ describe('cron', async () => {
|
||||
user.stats.mp = 120;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
|
||||
|
||||
@@ -1482,7 +1463,7 @@ describe('cron', async () => {
|
||||
|
||||
it('resets user progress', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.party.quest.progress.up).to.equal(0);
|
||||
expect(user.party.quest.progress.down).to.equal(0);
|
||||
@@ -1491,7 +1472,7 @@ describe('cron', async () => {
|
||||
|
||||
it('applies the user progress', async () => {
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
@@ -1529,19 +1510,19 @@ describe('cron', async () => {
|
||||
describe('login incentives', async () => {
|
||||
it('increments incentive counter each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
});
|
||||
|
||||
it('pushes a notification of the day\'s incentive each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
@@ -1549,13 +1530,13 @@ describe('cron', async () => {
|
||||
|
||||
it('replaces previous notifications', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
||||
@@ -1566,7 +1547,7 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
|
||||
daysMissed = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
@@ -1574,14 +1555,14 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards user bard robes if login incentive is 1', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
||||
@@ -1591,7 +1572,7 @@ describe('cron', async () => {
|
||||
it('awards user incentive backgrounds if login incentive is 2', async () => {
|
||||
user.loginIncentives = 1;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
expect(user.purchased.background.blue).to.eql(true);
|
||||
@@ -1605,7 +1586,7 @@ describe('cron', async () => {
|
||||
it('awards user Bard Hat if login incentive is 3', async () => {
|
||||
user.loginIncentives = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(3);
|
||||
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
||||
@@ -1615,7 +1596,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
|
||||
user.loginIncentives = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(4);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1625,7 +1606,7 @@ describe('cron', async () => {
|
||||
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
|
||||
user.loginIncentives = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(5);
|
||||
|
||||
@@ -1639,7 +1620,7 @@ describe('cron', async () => {
|
||||
it('awards user moon quest if login incentive is 7', async () => {
|
||||
user.loginIncentives = 6;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(7);
|
||||
expect(user.items.quests.moon1).to.eql(1);
|
||||
@@ -1649,7 +1630,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
|
||||
user.loginIncentives = 9;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(10);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1659,7 +1640,7 @@ describe('cron', async () => {
|
||||
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
|
||||
user.loginIncentives = 13;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(14);
|
||||
|
||||
@@ -1673,7 +1654,7 @@ describe('cron', async () => {
|
||||
it('awards user a bard instrument if login incentive is 18', async () => {
|
||||
user.loginIncentives = 17;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(18);
|
||||
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
||||
@@ -1683,7 +1664,7 @@ describe('cron', async () => {
|
||||
it('awards user second moon quest if login incentive is 22', async () => {
|
||||
user.loginIncentives = 21;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(22);
|
||||
expect(user.items.quests.moon2).to.eql(1);
|
||||
@@ -1693,7 +1674,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
|
||||
user.loginIncentives = 25;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(26);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1703,7 +1684,7 @@ describe('cron', async () => {
|
||||
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
|
||||
user.loginIncentives = 29;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(30);
|
||||
|
||||
@@ -1718,7 +1699,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
|
||||
user.loginIncentives = 34;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(35);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1728,7 +1709,7 @@ describe('cron', async () => {
|
||||
it('awards user the third moon quest if login incentive is 40', async () => {
|
||||
user.loginIncentives = 39;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(40);
|
||||
expect(user.items.quests.moon3).to.eql(1);
|
||||
@@ -1738,7 +1719,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
|
||||
user.loginIncentives = 44;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(45);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1748,7 +1729,7 @@ describe('cron', async () => {
|
||||
it('awards user a saddle if login incentive is 50', async () => {
|
||||
user.loginIncentives = 49;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
user, tasksByType, daysMissed,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(50);
|
||||
expect(user.items.food.Saddle).to.eql(1);
|
||||
@@ -1766,7 +1747,6 @@ describe('cron wrapper', () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable global-require */
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
import worker from '../../../../website/server/libs/worker';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
@@ -127,7 +127,7 @@ describe('emails', () => {
|
||||
let sendTxn = null;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(worker, 'sendJob').returns(defer().promise);
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('IS_PROD').returns(true);
|
||||
@@ -149,12 +149,13 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -167,7 +168,7 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(worker.sendJob).not.to.be.called;
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
@@ -232,12 +233,13 @@ describe('emails', () => {
|
||||
const mailingInfo = getUser();
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -251,14 +253,15 @@ describe('emails', () => {
|
||||
const variables = [];
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
&& value[0].vars[1].name === 'RECIPIENT_UNSUB_URL', 'matches personal variables'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
|
||||
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('localAnalytics', () => {
|
||||
let user;
|
||||
let localAnalytics;
|
||||
before(() => {
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
|
||||
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
|
||||
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
email: 'email@example.com',
|
||||
},
|
||||
},
|
||||
registeredThrough: 'habitica-web',
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackRegistrationEvent', () => {
|
||||
afterEach(async () => {
|
||||
await RegistrationEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a registration event when a user registers', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000001';
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(1);
|
||||
expect(registrationEvents[0]).to.have.property('userId', user._id);
|
||||
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
|
||||
});
|
||||
|
||||
it('saves the correct data to the database', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000002';
|
||||
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSubscriptionEvent', () => {
|
||||
afterEach(async () => {
|
||||
await SubscriptionEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a subscription event when a user subscribes', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000003';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'subscribed',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_123',
|
||||
planId: 'plan_123',
|
||||
});
|
||||
|
||||
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
|
||||
expect(subscriptionEvents).to.have.lengthOf(1);
|
||||
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
|
||||
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
|
||||
});
|
||||
|
||||
it('creates a subscription event with cancellation reason when a user cancels', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000004';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'cancelled',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_456',
|
||||
planId: 'plan_456',
|
||||
cancellationReason: 'No longer needed',
|
||||
});
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
|
||||
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,6 @@ import moment from 'moment';
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import common from '../../../../../website/common';
|
||||
import api from '../../../../../website/server/libs/payments/payments';
|
||||
import * as analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import * as notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -36,8 +36,6 @@ describe('payments/index', () => {
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
@@ -97,6 +95,16 @@ describe('payments/index', () => {
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvent).to.have.property('userId', recipient._id);
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('adds extra months to an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
@@ -298,28 +306,6 @@ describe('payments/index', () => {
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
@@ -455,6 +441,16 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('ipAddress');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
@@ -543,29 +539,24 @@ describe('payments/index', () => {
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
|
||||
});
|
||||
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom = { key: 'basic_earned' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -608,6 +599,23 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -1136,6 +1144,15 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analytics middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches analytics object to res', () => {
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics).to.exist;
|
||||
});
|
||||
|
||||
it('attaches stubbed methods for non-prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
|
||||
});
|
||||
|
||||
it('attaches real methods for prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,8 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -43,7 +44,8 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is an not "true"', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -55,7 +57,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -77,7 +80,8 @@ describe('rateLimiter middleware', () => {
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -92,7 +96,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
req.query.liveliness = 'abc';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -107,7 +112,8 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -124,7 +130,8 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -140,7 +147,8 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
req.query.liveliness = '';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -156,7 +164,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
// call for 31 times
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
@@ -180,7 +189,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -210,7 +220,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for registration calls with and without user id', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
@@ -241,7 +252,8 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for unauthenticated API calls', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
SPAM_WINDOW_LENGTH,
|
||||
MAX_CHAT_COUNT,
|
||||
MAX_SUBBED_GROUP_CHAT_COUNT,
|
||||
INVITES_LIMIT,
|
||||
model as Group,
|
||||
} from '../../../../website/server/models/group';
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
import * as email from '../../../../website/server/libs/email';
|
||||
import { TAVERN_ID } from '../../../../website/common/script/constants';
|
||||
import shared from '../../../../website/common';
|
||||
import { chatModel as Chat } from '../../../../website/server/models/message';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party; let questLeader; let participatingMember;
|
||||
@@ -1356,6 +1359,29 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEffectiveChatLimit', () => {
|
||||
it('returns the correct chat limit', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit()).to.eql(MAX_CHAT_COUNT);
|
||||
});
|
||||
|
||||
it('returns the passed limit if it is lower than the max', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit(10)).to.eql(10);
|
||||
});
|
||||
|
||||
it('returns the max if the passed limit is higher', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit(MAX_CHAT_COUNT + 10)).to.eql(MAX_CHAT_COUNT);
|
||||
});
|
||||
|
||||
it('returns the max for group plans', () => {
|
||||
const group = new Group();
|
||||
group.purchased.plan.customerId = '110002222333';
|
||||
expect(group.getEffectiveChatLimit()).to.eql(MAX_SUBBED_GROUP_CHAT_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendChat', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(User, 'updateOne');
|
||||
@@ -1462,6 +1488,34 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trimChat', () => {
|
||||
it('Only checks last message when not enough messages to trim', async () => {
|
||||
sandbox.spy(Chat, 'find');
|
||||
sandbox.spy(Chat, 'deleteMany');
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await party.trimChat();
|
||||
|
||||
expect(Chat.find).to.be.calledOnce;
|
||||
expect(Chat.deleteMany).to.not.be.called;
|
||||
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(3);
|
||||
});
|
||||
it('Deletes messages over the limit', async () => {
|
||||
sandbox.spy(Chat, 'find');
|
||||
sandbox.spy(Chat, 'deleteMany');
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
|
||||
await party.trimChat(1);
|
||||
|
||||
expect(Chat.find).to.be.calledOnce;
|
||||
expect(Chat.deleteMany).to.be.calledOnce;
|
||||
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#startQuest', () => {
|
||||
context('Failure Conditions', () => {
|
||||
it('throws an error if group is not a party', async () => {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /analytics/track/:eventName', () => {
|
||||
it('calls res.analytics', async () => {
|
||||
const user = await generateUser();
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
|
||||
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,23 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not accept invite for a quest already underway', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -193,6 +193,23 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
expect(questingGroup.quest.members[notInPartyUser._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who have been deleted from quest.members', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].del('/user', {
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who don\'t have true value in quest.members from quest.members', async () => {
|
||||
const partyMemberThatRejects = partyMembers[1];
|
||||
const partyMemberThatIgnores = partyMembers[2];
|
||||
|
||||
@@ -100,6 +100,23 @@ describe('POST /groups/:groupId/quests/reject', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user rejected', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(false);
|
||||
});
|
||||
|
||||
it('return an error when a user rejects an invite already accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import {
|
||||
each,
|
||||
map,
|
||||
} from 'lodash';
|
||||
import {
|
||||
checkExistence,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateUser,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
@@ -9,7 +15,6 @@ import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import sendJob from '../../../../../website/server/libs/worker';
|
||||
|
||||
const DELETE_CONFIRMATION = 'DELETE';
|
||||
|
||||
@@ -42,13 +47,12 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends deletion job to worker', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
it('deletes the user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
@@ -80,6 +84,53 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user\'s tasks', async () => {
|
||||
await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
// gets the user's tasks ids
|
||||
const ids = [];
|
||||
each(user.tasksOrder, idsForOrder => {
|
||||
ids.push(...idsForOrder);
|
||||
});
|
||||
|
||||
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
|
||||
});
|
||||
|
||||
it('reduces memberCount in challenges user is linked to', async () => {
|
||||
const populatedGroup = await createAndPopulateGroup({
|
||||
members: 2,
|
||||
});
|
||||
|
||||
const { group } = populatedGroup;
|
||||
const authorizedUser = populatedGroup.members[1];
|
||||
|
||||
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
||||
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(2);
|
||||
|
||||
await authorizedUser.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
@@ -107,10 +158,10 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const textPassword = 'mySecretPassword';
|
||||
const salt = sha1MakeSalt();
|
||||
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
|
||||
await user.updateOne({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
@@ -128,8 +179,7 @@ describe('DELETE /user', () => {
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
@@ -163,12 +213,11 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes a Google user', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -183,13 +232,12 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes an Apple user', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
it('deletes a Apple user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -23,15 +22,4 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
|
||||
import { encrypt } from '../../../../../../website/server/libs/encryption';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
function generateRandomUserName () {
|
||||
return (Date.now() + uuid()).substring(0, 20);
|
||||
@@ -41,6 +42,25 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
const user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
|
||||
});
|
||||
|
||||
it('registers a new user and sets verifiedUsername to true', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
@@ -65,6 +66,19 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const socialUser = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', socialUser.id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
|
||||
it('includes sanitized version of provided username', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -231,6 +245,17 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not track a registration event for existing users', async () => {
|
||||
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
||||
@@ -13,7 +13,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -32,12 +31,6 @@ describe('shared.ops.buy', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', async () => {
|
||||
@@ -51,10 +44,8 @@ describe('shared.ops.buy', () => {
|
||||
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
await buy(user, { params: { key: 'potion' } });
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', async () => {
|
||||
|
||||
@@ -29,10 +29,9 @@ describe('shared.ops.buyArmoire', () => {
|
||||
const YIELD_EQUIPMENT = 0.5;
|
||||
const YIELD_FOOD = 0.7;
|
||||
const YIELD_EXP = 0.9;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyArmoire (_user, _req, _analytics) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
|
||||
async function buyArmoire (_user, _req) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -50,12 +49,10 @@ describe('shared.ops.buyArmoire', () => {
|
||||
user.items.food = {};
|
||||
|
||||
sandbox.stub(randomValFns, 'trueRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
randomValFns.trueRandom.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -147,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
await buyArmoire(user, {}, analytics);
|
||||
await buyArmoire(user, {});
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(3);
|
||||
|
||||
@@ -155,7 +152,6 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(analytics.track).to.be.calledTwice;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
@@ -11,15 +10,14 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
|
||||
async function buyGem (user, req, analytics) {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
async function buyGem (user, req) {
|
||||
const buyOp = new BuyGemOperation(user, req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyGem', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
const goldPoints = 40;
|
||||
const gemsBought = 40;
|
||||
const userGemAmount = 10;
|
||||
@@ -35,23 +33,16 @@ describe('shared.ops.buyGem', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Gems', () => {
|
||||
it('purchases gems', async () => {
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', async () => {
|
||||
|
||||
@@ -10,10 +10,9 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyHealthPotion', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyHealthPotion (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
|
||||
async function buyHealthPotion (_user, _req) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -32,19 +31,13 @@ describe('shared.ops.buyHealthPotion', () => {
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Potion', () => {
|
||||
it('recovers 15 hp', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buyHealthPotion(user, {}, analytics);
|
||||
await buyHealthPotion(user, {});
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('does not increase hp above 50', async () => {
|
||||
|
||||
@@ -13,15 +13,14 @@ import {
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
async function buyGear (user, req, analytics) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
async function buyGear (user, req) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -47,14 +46,12 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
@@ -65,7 +62,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
it('adds equipment to inventory', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
@@ -92,13 +89,12 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
@@ -111,7 +107,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -27,11 +26,9 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -93,7 +90,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
context('successful purchases', () => {
|
||||
it('buys Steampunk Accessories Set', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
|
||||
await buyMysterySet(user, { params: { key: '301404' } });
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
@@ -106,7 +103,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
it('buys mystery set if it is available', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||
await buyMysterySet(user, { params: { key: '201601' } });
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||
|
||||
@@ -12,10 +12,9 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -25,13 +24,11 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
@@ -12,21 +12,15 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyQuest', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('buys a Quest scroll', async () => {
|
||||
@@ -35,12 +29,11 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||
@@ -49,10 +42,9 @@ describe('shared.ops.buyQuest', () => {
|
||||
user.items.quests[key] = -1;
|
||||
await buyQuest(user, {
|
||||
params: { key },
|
||||
}, analytics);
|
||||
});
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
|
||||
@@ -61,13 +53,13 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: '3',
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 4,
|
||||
@@ -82,7 +74,7 @@ describe('shared.ops.buyQuest', () => {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: 'a',
|
||||
}, analytics);
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -187,12 +179,11 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress3: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,20 +14,17 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
async function buySpecialSpell (_user, _req) {
|
||||
const buyOp = new BuySpellOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -78,7 +75,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
@@ -89,7 +86,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a limited card when it is available', async () => {
|
||||
@@ -101,7 +97,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.nye).to.equal(1);
|
||||
@@ -112,7 +108,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the card is not currently available', async () => {
|
||||
@@ -140,7 +135,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
}, analytics);
|
||||
});
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.seafoam).to.equal(1);
|
||||
@@ -151,7 +146,6 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the spell is not currently available', async () => {
|
||||
|
||||
@@ -13,21 +13,15 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
|
||||
|
||||
describe('common.ops.hourglassPurchase', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyMount (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
|
||||
async function buyMount (_user, _req) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -131,12 +125,11 @@ describe('common.ops.hourglassPurchase', () => {
|
||||
it('buys a pet', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 2;
|
||||
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
|
||||
|
||||
expect(message).to.eql(i18n.t('hourglassPurchase'));
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a mount', async () => {
|
||||
|
||||
@@ -17,20 +17,17 @@ describe('shared.ops.purchase', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
before(() => {
|
||||
user = generateUser({ 'stats.class': 'rogue' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
@@ -187,11 +184,10 @@ describe('shared.ops.purchase', () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
await purchase(user, { params: { type, key } }, analytics);
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases hatchingPotions', async () => {
|
||||
@@ -332,7 +328,7 @@ describe('shared.ops.purchase', () => {
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -345,7 +341,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: -2 });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -358,7 +354,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
|
||||
@@ -54,19 +54,4 @@ describe('armoire', () => {
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
if (setMap[item.set] === undefined) {
|
||||
setMap[item.set] = 0;
|
||||
}
|
||||
setMap[item.set] += 1;
|
||||
});
|
||||
Object.keys(setMap).forEach(set => {
|
||||
expect(setMap[set], set).to.be.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/paypal') === 0
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
|
||||
@@ -12,20 +12,12 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
// TODO find a way to let eslint understand webpack aliases
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': 'off',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
// this creates issues with the current way we have to push the process.env vars to webpack
|
||||
// https://github.com/eslint/eslint/issues/14918
|
||||
// https://github.com/webpack/webpack/issues/5392
|
||||
// off for now, because any eslint --fix will then still do it anyway
|
||||
// maybe this can be turned on again once we switch to newer vue/vite
|
||||
// Important! process.env.XYZ should not be destructured
|
||||
'prefer-destructuring': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Habitica - FAQ</title>
|
||||
<meta name="description" content="Frequently Asked Questions about Habitica, the gamified task manager.">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
||||
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
<link rel="mask-icon" href="/static/icons/favicon.ico">
|
||||
<meta property="og:image" content="/static/emails/images/meta-image.png" />
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||
<!-- Translations -->
|
||||
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+20
-589
@@ -41,7 +41,6 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -55,9 +54,7 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
@@ -2111,8 +2108,9 @@
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
@@ -3634,41 +3632,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
|
||||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -3679,8 +3648,9 @@
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3876,181 +3846,6 @@
|
||||
"vue-template-compiler": "^2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
||||
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
||||
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
||||
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
|
||||
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
||||
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
|
||||
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
|
||||
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
|
||||
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
||||
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
|
||||
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-section": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-opt": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1",
|
||||
"@webassemblyjs/wast-printer": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
|
||||
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
|
||||
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
|
||||
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
|
||||
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
@@ -4098,48 +3893,6 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/amplitude-js": {
|
||||
"version": "8.21.9",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
|
||||
@@ -4617,8 +4370,9 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
@@ -4783,16 +4537,6 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -4859,8 +4603,9 @@
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -5196,20 +4941,6 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||
@@ -6384,16 +6115,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
@@ -6871,13 +6592,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -6921,13 +6635,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/habitica-markdown": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
|
||||
@@ -7726,37 +7433,6 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
@@ -7841,13 +7517,6 @@
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -7911,20 +7580,6 @@
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
|
||||
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -8128,13 +7783,6 @@
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -8458,13 +8106,6 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@@ -9504,63 +9145,6 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
|
||||
@@ -9838,8 +9422,9 @@
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
@@ -10130,20 +9715,6 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-mini": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
|
||||
@@ -10154,8 +9725,9 @@
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -10169,47 +9741,13 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -10585,8 +10123,9 @@
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
@@ -10965,15 +10504,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/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==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.5.16"
|
||||
}
|
||||
},
|
||||
"node_modules/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",
|
||||
@@ -11041,20 +10571,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@@ -11065,91 +10581,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.102.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.26.3",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"webpack-cli": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -60,8 +59,6 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,68 @@
|
||||
.quest_lostMasterclasser4 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_solarSystem {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_virtualpet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
|
||||
.Pet_HatchingPotion_Alien {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_VirtualPet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Fungi {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -26,7 +91,6 @@
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -34,6 +98,7 @@
|
||||
|
||||
/* Critical */
|
||||
.weapon_special_critical {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-left:-12px;
|
||||
@@ -44,7 +109,6 @@
|
||||
.weapon_special_1 {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -53,15 +117,36 @@
|
||||
.back_special_heroicAureole {
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.broad_armor_special_0,.slim_armor_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.broad_armor_special_1,.slim_armor_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
|
||||
@@ -69,17 +154,36 @@
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.eyewear_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.Pet-Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
|
||||
.Pet-Gryphatrice-Jubilant {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -89,11 +193,39 @@
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.background_airship {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_clocktower {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_steamworks {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
@@ -695,6 +695,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_beach_with_volcano {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beach_with_volcano.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_beehive {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beehive.png');
|
||||
width: 141px;
|
||||
@@ -2346,6 +2351,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_tropical_coral_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tropical_coral_garden.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_tulip_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tulip_garden.png');
|
||||
width: 141px;
|
||||
@@ -2401,6 +2411,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_vegetable_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_vegetable_garden.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_viking_ship {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_viking_ship.png');
|
||||
width: 141px;
|
||||
@@ -29880,6 +29895,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_kendoBogu {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_kendoBogu.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_lamplightersGreatcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lamplightersGreatcoat.png');
|
||||
width: 114px;
|
||||
@@ -30535,6 +30555,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_kendoMen {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_kendoMen.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_lamplightersTopHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lamplightersTopHat.png');
|
||||
width: 114px;
|
||||
@@ -30920,6 +30945,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_gardenHose {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenHose.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_gardenersSpade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png');
|
||||
width: 114px;
|
||||
@@ -31550,6 +31580,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_kendoBogu {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_kendoBogu.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_lamplightersGreatcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lamplightersGreatcoat.png');
|
||||
width: 114px;
|
||||
@@ -31930,6 +31965,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_brightRainbowKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_brightRainbowKite.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_buoyantBubbles {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_buoyantBubbles.png');
|
||||
width: 114px;
|
||||
@@ -32030,6 +32070,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gardenRake {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenRake.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gardenersWateringCan {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png');
|
||||
width: 114px;
|
||||
@@ -32125,6 +32170,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_kendoShinai {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_kendoShinai.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_lamplighter {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_lamplighter.png');
|
||||
width: 114px;
|
||||
@@ -32210,6 +32260,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pastelRainbowKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pastelRainbowKite.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pinkKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pinkKite.png');
|
||||
width: 114px;
|
||||
@@ -34200,6 +34255,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.eyewear_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34220,11 +34280,31 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202607 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202607.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202608 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202608.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.slim_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34250,6 +34330,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202607 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202607.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_mystery_202608 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202608.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.back_mystery_201402 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
|
||||
width: 90px;
|
||||
@@ -37715,6 +37805,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -37965,6 +38075,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38155,6 +38285,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.shield_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38395,6 +38540,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38635,6 +38800,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png');
|
||||
width: 90px;
|
||||
|
||||
@@ -42,6 +42,7 @@ ul {
|
||||
font-weight: 400;
|
||||
line-height: 1.75;
|
||||
color: $purple-200;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.d-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{{ $t('tavernDiscontinued') }}
|
||||
</h1>
|
||||
<p>{{ $t('tavernDiscontinuedDetail') }}</p>
|
||||
<p v-html="$t('tavernDiscontinuedLinks')"></p>
|
||||
<p v-html="$t('tavernDiscontinuedLinks', tavernLinks)"></p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>
|
||||
@@ -38,6 +38,15 @@
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
tavernLinks: {
|
||||
faqLink: '<a href="/static/faq/tavern-and-guilds">',
|
||||
homeLink: '<a href="/">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn']),
|
||||
retiredChatPage () {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<div
|
||||
v-once
|
||||
class="opt-out-description"
|
||||
v-html="$t('optOutOfClassesText')"
|
||||
v-html="$t('optOutOfClassesText', optOutLinks)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,6 +214,10 @@ export default {
|
||||
wizard: wizardIcon,
|
||||
}),
|
||||
selectedClass: 'warrior',
|
||||
optOutLinks: {
|
||||
linkOpen: '<a href="/static/faq#what-classes" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<br>
|
||||
<a
|
||||
:href="$t('conRewardsURL')"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>{{ $t('contribLink') }}</a>
|
||||
<br>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</button>
|
||||
<h4
|
||||
class="text-center"
|
||||
v-html="$t('dyingOftenTips')"
|
||||
v-html="$t('dyingOftenTips', tipLinks)"
|
||||
></h4>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,6 +86,10 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
tipLinks: {
|
||||
linkOpen: '<a href="/static/faq#prevent-damage" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
&& user.achievements.ultimateGearSets.rogue
|
||||
&& user.achievements.ultimateGearSets.warrior"
|
||||
>
|
||||
<p v-html="$t('moreGearAchievements')"></p>
|
||||
<p v-html="$t('moreGearAchievements', gearAchievementLinks)"></p>
|
||||
<br>
|
||||
</div>
|
||||
<Sprite image-name="shop_armoire" />
|
||||
@@ -95,6 +95,14 @@ export default {
|
||||
achievementAvatar,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
gearAchievementLinks: {
|
||||
linkOpen: '<a href="/user/settings/site" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
|
||||
@@ -108,15 +108,15 @@ export default {
|
||||
const allEmails = [];
|
||||
if (user.auth.local.email) allEmails.push(user.auth.local.email);
|
||||
if (user.auth.google && user.auth.google.emails) {
|
||||
const emails = user.auth.google.emails;
|
||||
const { emails } = user.auth.google;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.apple && user.auth.apple.emails) {
|
||||
const emails = user.auth.apple.emails;
|
||||
const { emails } = user.auth.apple;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||
const emails = user.auth.facebook.emails;
|
||||
const { emails } = user.auth.facebook;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
return allEmails;
|
||||
|
||||
+4
-1
@@ -491,6 +491,7 @@
|
||||
v-if="hero.purchased.plan.paymentMethod === 'Google'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="playOrdersUrl"
|
||||
>
|
||||
Play Console
|
||||
@@ -499,6 +500,7 @@
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
|
||||
>
|
||||
PayPal Dashboard
|
||||
@@ -507,6 +509,7 @@
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
|
||||
>
|
||||
Stripe Dashboard
|
||||
@@ -609,7 +612,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>{{ $t('mobileIOS') }}
|
||||
</a>
|
||||
@@ -18,6 +19,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>{{ $t('mobileAndroid') }}
|
||||
</a>
|
||||
@@ -52,6 +54,7 @@
|
||||
<a
|
||||
href="https://habitica.wordpress.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('companyBlog') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -71,6 +74,7 @@
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/static/community-guidelines"
|
||||
>{{ $t('communityGuidelines') }}
|
||||
</a>
|
||||
@@ -84,6 +88,7 @@
|
||||
<a
|
||||
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('companyContribute') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -91,6 +96,7 @@
|
||||
<a
|
||||
href="https://translate.habitica.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('translateHabitica') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -111,6 +117,7 @@
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
{{ $t('reportBug') }}
|
||||
@@ -122,6 +129,7 @@
|
||||
<a
|
||||
href="mailto:admin@habitica.com?subject=Habitica Web Bug Report"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('reportBug') }}
|
||||
</a>
|
||||
@@ -130,6 +138,7 @@
|
||||
<a
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('requestFeature') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -143,6 +152,7 @@
|
||||
<a
|
||||
href="/apidoc"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('APIv3') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -150,6 +160,7 @@
|
||||
<a
|
||||
:href="getDataDisplayToolUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('dataDisplayTool') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -157,6 +168,7 @@
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('guidanceForBlacksmiths') }}
|
||||
</a>
|
||||
</li>
|
||||
@@ -172,6 +184,7 @@
|
||||
class="social-circle mr-2"
|
||||
href="https://www.instagram.com/habitica/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon"
|
||||
@@ -181,6 +194,7 @@
|
||||
<a
|
||||
href="https://www.instagram.com/habitica/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('communityInstagram') }}
|
||||
</a>
|
||||
@@ -190,6 +204,7 @@
|
||||
class="social-circle mr-2"
|
||||
href="https://bsky.app/profile/habitica.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon"
|
||||
@@ -199,6 +214,7 @@
|
||||
<a
|
||||
href="https://bsky.app/profile/habitica.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Bluesky
|
||||
</a>
|
||||
@@ -208,6 +224,7 @@
|
||||
class="social-circle mr-2"
|
||||
href="https://www.facebook.com/Habitica/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon"
|
||||
@@ -217,6 +234,7 @@
|
||||
<a
|
||||
href="https://www.facebook.com/Habitica/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('communityFacebook') }}
|
||||
</a>
|
||||
@@ -226,6 +244,7 @@
|
||||
class="social-circle mr-2"
|
||||
href="http://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div
|
||||
class="social-icon svg-icon"
|
||||
@@ -235,6 +254,7 @@
|
||||
<a
|
||||
href="http://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('tumblr') }}
|
||||
</a>
|
||||
@@ -266,12 +286,14 @@
|
||||
<span class="privacy-policy">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/static/privacy"
|
||||
>{{ $t('privacy') }}</a>
|
||||
</span>
|
||||
<span class="terms">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/static/terms"
|
||||
>{{ $t('terms') }}</a>
|
||||
</span>
|
||||
@@ -287,12 +309,14 @@
|
||||
<div class="privacy-policy mx-auto mb-2">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/static/privacy"
|
||||
>{{ $t('privacy') }}</a>
|
||||
</div>
|
||||
<div class="mobile-terms mx-auto mb-2">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/static/terms"
|
||||
>{{ $t('terms') }}</a>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,29 @@
|
||||
class="form mx-auto"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<div v-if="needsEmailField">
|
||||
<input
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailAddress')"
|
||||
:class="{
|
||||
'mb-3': !emailError,
|
||||
'input-invalid input-with-error mb-2': emailError,
|
||||
'input-valid': email && emailValid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
<p class="purple-600 mb-3">
|
||||
{{ $t('emailRequiredForSupport') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
@@ -54,12 +77,13 @@
|
||||
v-once
|
||||
class="custom-control-label purple-600"
|
||||
for="privacyTOS"
|
||||
v-html="$t('acceptPrivacyTOS')"
|
||||
v-html="$t('acceptPrivacyTOS', acceptLinks)"
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!username || usernameInvalid || !privacyAccepted"
|
||||
class="btn btn-info d-flex justify-content-center
|
||||
align-items-center w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
@@ -133,10 +157,12 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,23 +174,24 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [sanitizeRedirect],
|
||||
mixins: [accountCreation, sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
privacyAccepted: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
needsEmailField: false,
|
||||
acceptLinks: {
|
||||
termsLink: '<a href="/static/terms" target="_blank" rel="noreferrer noopener">',
|
||||
privacyLink: '<a href="/static/privacy" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -183,30 +210,40 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (!this.email) {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
if (!this.email) {
|
||||
this.email = window.sessionStorage.getItem('apple-email');
|
||||
}
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
|
||||
if (!this.email && this.registrationMethod !== 'apple') {
|
||||
return;
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
|
||||
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
|
||||
this.needsEmailField = true;
|
||||
}
|
||||
if (this.email) {
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById('usernameInput').focus();
|
||||
},
|
||||
methods: {
|
||||
@@ -237,6 +274,7 @@ export default {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<p
|
||||
v-once
|
||||
class="w-50 mx-auto"
|
||||
v-html="$t('visitCustomizationsShop')"
|
||||
v-html="$t('visitCustomizationsShop', customizeLinks)"
|
||||
></p>
|
||||
</div>
|
||||
<customize-banner
|
||||
@@ -104,6 +104,10 @@ export default {
|
||||
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
|
||||
},
|
||||
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
|
||||
customizeLinks: {
|
||||
linkOpen: '<a href="/shops/customizations">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<p
|
||||
v-once
|
||||
class="w-50 mx-auto"
|
||||
v-html="$t('visitCustomizationsShop')"
|
||||
v-html="$t('visitCustomizationsShop', customizeLinks)"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,6 +95,14 @@ export default {
|
||||
props: [
|
||||
'editing',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
customizeLinks: {
|
||||
linkOpen: '<a href="/shops/customizations">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hairSubMenuItems () {
|
||||
const items = [
|
||||
|
||||
@@ -217,8 +217,8 @@ export default {
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
this.cancel();
|
||||
return [];
|
||||
}
|
||||
this.currentSearch = regexRes[1];
|
||||
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
if (this.currentSearch.length === 0) return [];
|
||||
|
||||
|
||||
@@ -208,8 +208,8 @@ export default {
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ export default {
|
||||
return this.userGuilds.filter(group => {
|
||||
const leaderId = group.leader?._id || group.leader;
|
||||
if (leaderId !== this.user._id) return false;
|
||||
const purchased = group.purchased;
|
||||
const { purchased } = group;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (this.activeGroupPlanIds.includes(group._id)) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
@@ -492,7 +492,7 @@ export default {
|
||||
},
|
||||
isPartyPreviouslyUpgraded () {
|
||||
if (!this.userParty) return false;
|
||||
const purchased = this.userParty.purchased;
|
||||
const { purchased } = this.userParty;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
return new Date(purchased.dateTerminated) < new Date();
|
||||
@@ -533,7 +533,7 @@ export default {
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.upgradeableGuilds.length > 0) {
|
||||
this.selectedOption = this.upgradeableGuilds[0];
|
||||
[this.selectedOption] = this.upgradeableGuilds;
|
||||
} else if (this.upgradeableParty) {
|
||||
this.selectedOption = this.upgradeableParty;
|
||||
} else {
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</ul>
|
||||
<div
|
||||
class="mx-auto"
|
||||
v-html="$t('newGroupsVisitFAQ')"
|
||||
v-html="$t('newGroupsVisitFAQ', faqLinks)"
|
||||
></div>
|
||||
<div
|
||||
class="mx-auto"
|
||||
@@ -193,6 +193,10 @@ export default {
|
||||
close: closeIcon,
|
||||
sparkles,
|
||||
}),
|
||||
faqLinks: {
|
||||
linkOpen: '<a href="/static/faq#group-plans" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -198,7 +198,6 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
|
||||
import todoIcon from '@/assets/svg/todo.svg?raw';
|
||||
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
@@ -438,14 +437,6 @@ export default {
|
||||
return false;
|
||||
},
|
||||
changeMirrorPreference (newVal) {
|
||||
Analytics.track({
|
||||
eventName: 'mirror tasks',
|
||||
eventAction: 'mirror tasks',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div
|
||||
v-once
|
||||
class="col col-sm-12 col-xl-8"
|
||||
v-html="$t('communityGuidelinesIntro')"
|
||||
v-html="$t('communityGuidelinesIntro', guidelineLinks)"
|
||||
></div>
|
||||
<div class="col-md-auto col-md-12 col-xl-4">
|
||||
<button
|
||||
@@ -60,6 +60,14 @@
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
guidelineLinks: {
|
||||
linkOpen: '<a href="/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
communityGuidelinesAccepted () {
|
||||
|
||||
@@ -240,7 +240,6 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import closeX from '../ui/closeX';
|
||||
|
||||
@@ -276,11 +275,6 @@ export default {
|
||||
this.$store.state.party.data = party;
|
||||
this.user.party._id = party._id;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: party._id,
|
||||
partySize: 1,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
await this.$router.push('/party');
|
||||
},
|
||||
|
||||
@@ -314,7 +314,6 @@ import extend from 'lodash/extend';
|
||||
import groupUtilities from '@/mixins/groupsUtilities';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapGetters } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import participantListModal from './participantListModal';
|
||||
import groupFormModal from './groupFormModal';
|
||||
import groupGemsModal from '@/components/groups/groupGemsModal';
|
||||
@@ -560,7 +559,6 @@ export default {
|
||||
|
||||
if (this.isParty) {
|
||||
data.type = 'party';
|
||||
Analytics.updateUser({ partySize: null, partyID: null });
|
||||
this.$store.state.partyMembers = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
class="no-quest-to-start"
|
||||
>
|
||||
<b>{{ $t('noQuestToStartTitle') }}</b> <br>
|
||||
<span v-html="$t('noQuestToStart', { questShop: '/shops/quests' })"></span>
|
||||
<span v-html="$t('noQuestToStart', questLinks)"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -334,7 +334,6 @@ import orderBy from 'lodash/orderBy';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
|
||||
import questDialogContent from '../shops/quests/questDialogContent';
|
||||
@@ -370,6 +369,10 @@ export default {
|
||||
shareUserIdShown: false,
|
||||
quests,
|
||||
sortBy: 'AZ',
|
||||
questLinks: {
|
||||
linkOpen: '<a href="/shops/quests">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -421,11 +424,6 @@ export default {
|
||||
async questInit () {
|
||||
this.loading = true;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: this.group._id,
|
||||
partySize: this.group.memberCount,
|
||||
});
|
||||
|
||||
const groupId = this.group._id || this.user.party._id;
|
||||
|
||||
const key = this.selectedQuest;
|
||||
|
||||
@@ -1,889 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<world-boss-info-modal />
|
||||
<world-boss-rage-modal />
|
||||
<div class="col-12 col-sm-8 clearfix standard-page">
|
||||
<div class="row">
|
||||
<div class="col-6 title-details">
|
||||
<h1 v-once>
|
||||
{{ $t('welcomeToTavern') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<chat
|
||||
:label="$t('tavernChat')"
|
||||
:group="group"
|
||||
:placeholder="$t('tavernCommunityGuidelinesPlaceholder')"
|
||||
@fetchRecentMessages="fetchRecentMessages()"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4 sidebar">
|
||||
<div class="section">
|
||||
<div
|
||||
class="grassy-meadow-backdrop"
|
||||
:style="{'background-image': imageURLs.background}"
|
||||
>
|
||||
<div
|
||||
class="daniel_front"
|
||||
:style="{'background-image': imageURLs.npc}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="boss-section">
|
||||
<div
|
||||
v-if="group && group.quest && group.quest.active"
|
||||
class="world-boss"
|
||||
:style="{
|
||||
background: questData.colors.dark,
|
||||
'border-color': questData.colors.extralight,
|
||||
'outline-color': questData.colors.light}"
|
||||
>
|
||||
<div
|
||||
class="corner-decoration"
|
||||
:style="{top: '-2px', right: '-2px'}"
|
||||
></div>
|
||||
<div
|
||||
class="corner-decoration"
|
||||
:style="{top: '-2px', left: '-2px'}"
|
||||
></div>
|
||||
<div
|
||||
class="corner-decoration"
|
||||
:style="{bottom: '-2px', right: '-2px'}"
|
||||
></div>
|
||||
<div
|
||||
class="corner-decoration"
|
||||
:style="{bottom: '-2px', left: '-2px'}"
|
||||
></div>
|
||||
<div class="text-center float-bar d-flex align-items-center">
|
||||
<span class="diamond"></span>
|
||||
<span
|
||||
class="strong reduce"
|
||||
:style="{background: questData.colors.dark}"
|
||||
>{{ $t('worldBossEvent') }}</span>
|
||||
<span class="diamond"></span>
|
||||
</div>
|
||||
<div class="boss-gradient pb-3 pt-3">
|
||||
<p
|
||||
class="text-center reduce"
|
||||
:style="{color: questData.colors.extralight}"
|
||||
>
|
||||
{{ $t(`${questData.key}ArtCredit`) }}
|
||||
</p>
|
||||
<div
|
||||
class="quest-boss"
|
||||
:class="'background_' + questData.key"
|
||||
>
|
||||
<div
|
||||
class="quest-boss"
|
||||
:class="'quest_' + questData.key"
|
||||
></div>
|
||||
<div
|
||||
class="quest-boss"
|
||||
:class="'phobia_' + questData.key"
|
||||
:style="{display: 'none'}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<div class="row d-flex align-items-center mb-2">
|
||||
<div class="col-sm-6">
|
||||
<strong class="float-left">{{ questData.boss.name() }}</strong>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<span class="d-flex float-right">
|
||||
<div
|
||||
class="svg-icon boss-icon"
|
||||
v-html="icons.swordIcon"
|
||||
></div>
|
||||
<span
|
||||
class="ml-1 reduce"
|
||||
:style="{color: questData.colors.extralight}"
|
||||
>{{ $t('pendingDamage', {damage: pendingDamage()}) }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grey-progress-bar mb-1">
|
||||
<div
|
||||
class="boss-health-bar"
|
||||
:style="{width: (group.quest.progress.hp / questData.boss.hp) * 100 + '%'}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="d-flex align-items-center">
|
||||
<div
|
||||
class="svg-icon boss-icon"
|
||||
v-html="icons.healthIcon"
|
||||
></div>
|
||||
<span
|
||||
class="reduce ml-1 pt-1"
|
||||
>{{ $t('bossHealth', {
|
||||
currentHealth: bossCurrentHealth(),
|
||||
maxHealth: questData.boss.hp.toLocaleString()}) }}</span>
|
||||
</span>
|
||||
<div class="mt-3 mb-2">
|
||||
<strong class="mr-1">{{ $t('rageAttack') }}</strong>
|
||||
<span>{{ questData.boss.rage.title() }}</span>
|
||||
</div>
|
||||
<div class="grey-progress-bar mb-1">
|
||||
<div
|
||||
class="boss-health-bar rage-bar"
|
||||
:style="{
|
||||
width: (group.quest.progress.rage / questData.boss.rage.value) * 100 + '%'}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="d-flex align-items-center">
|
||||
<div
|
||||
class="svg-icon boss-icon"
|
||||
v-html="icons.rageIcon"
|
||||
></div>
|
||||
<span
|
||||
class="reduce ml-1 pt-1"
|
||||
>{{ $t('bossRage', {
|
||||
currentRage: bossCurrentRage(),
|
||||
maxRage: questData.boss.rage.value.toLocaleString()}) }}</span>
|
||||
</span>
|
||||
<div class="row d-flex align-items-center mb-2 mt-2">
|
||||
<div class="col-sm-4 d-flex">
|
||||
<strong class="mr-2">{{ $t('rageStrikes') }}</strong>
|
||||
<div
|
||||
v-b-tooltip.hover.top="questData.boss.rage.description()"
|
||||
class="svg-icon boss-icon information-icon m-auto"
|
||||
v-html="icons.informationIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col-sm-8 d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
class="m-auto"
|
||||
@click="showWorldBossRage('seasonalShop')"
|
||||
>
|
||||
<img
|
||||
v-if="!group.quest.extra.worldDmg.seasonalShop"
|
||||
class="rage-strike"
|
||||
src="@/assets/images/world-boss/rage_strike@2x.png"
|
||||
>
|
||||
<img
|
||||
v-if="group.quest.extra.worldDmg.seasonalShop"
|
||||
class="rage-strike-active"
|
||||
src="@/assets/images/world-boss/rage_strike-seasonalShop@2x.png"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="m-auto"
|
||||
@click="showWorldBossRage('market')"
|
||||
>
|
||||
<img
|
||||
v-if="!group.quest.extra.worldDmg.market"
|
||||
class="rage-strike"
|
||||
src="@/assets/images/world-boss/rage_strike@2x.png"
|
||||
>
|
||||
<img
|
||||
v-if="group.quest.extra.worldDmg.market"
|
||||
class="rage-strike-active"
|
||||
src="@/assets/images/world-boss/rage_strike-market@2x.png"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="m-auto"
|
||||
@click="showWorldBossRage('quests')"
|
||||
>
|
||||
<img
|
||||
v-if="!group.quest.extra.worldDmg.quests"
|
||||
class="rage-strike"
|
||||
src="@/assets/images/world-boss/rage_strike@2x.png"
|
||||
>
|
||||
<img
|
||||
v-if="group.quest.extra.worldDmg.quests"
|
||||
class="rage-strike-active"
|
||||
src="@/assets/images/world-boss/rage_strike-quests@2x.png"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="boss-description p-3"
|
||||
:style="{'border-color': questData.colors.extralight}"
|
||||
@click="sections.worldBoss = !sections.worldBoss"
|
||||
>
|
||||
<strong class="float-left">{{ $t('worldBossDescription') }}</strong>
|
||||
<div class="float-right">
|
||||
<div
|
||||
v-if="!sections.worldBoss"
|
||||
class="toggle-down"
|
||||
>
|
||||
<div
|
||||
class="svg-icon boss-icon"
|
||||
v-html="icons.chevronIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="sections.worldBoss"
|
||||
class="toggle-up"
|
||||
>
|
||||
<div
|
||||
class="svg-icon boss-icon reverse"
|
||||
v-html="icons.chevronIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="sections.worldBoss"
|
||||
class="mt-3"
|
||||
v-html="questData.notes()"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- .text-center.mt-4.world-boss
|
||||
-info-button(@click="showWorldBossInfo()") {{$t('whatIsWorldBoss') }}
|
||||
-->
|
||||
</div>
|
||||
<div class="sleep px-4 py-3">
|
||||
<strong v-once>{{ $t('sleepDescription') }}</strong>
|
||||
<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="btn btn-secondary pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('pauseDailies') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="user.preferences.sleep"
|
||||
v-once
|
||||
class="btn btn-secondary pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('unpauseDailies') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<sidebar-section :title="$t('staff')">
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="user in staff"
|
||||
:key="user.uuid"
|
||||
class="col-6 staff"
|
||||
:class="{
|
||||
staff: user.type === 'Staff',
|
||||
moderator: user.type === 'Moderator'}"
|
||||
>
|
||||
<div>
|
||||
<router-link
|
||||
class="title"
|
||||
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
|
||||
>
|
||||
{{ user.name }}
|
||||
</router-link>
|
||||
<div
|
||||
v-if="user.type === 'Staff'"
|
||||
class="svg-icon staff-icon"
|
||||
v-html="icons.tierStaff"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</sidebar-section>
|
||||
<sidebar-section :title="$t('helpfulLinks')">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="mailto:admin@habitica.com">
|
||||
{{ $t('reportCommunityIssues') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-once
|
||||
to="/static/community-guidelines"
|
||||
>
|
||||
{{ $t('communityGuidelines') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
to="/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601"
|
||||
>
|
||||
{{ $t('lookingForGroup') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
v-once
|
||||
to="/static/faq"
|
||||
>
|
||||
{{ $t('faq') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href
|
||||
:style="glossary-link"
|
||||
v-html="$t('glossary')"
|
||||
></a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
v-once
|
||||
href="https://habitica.fandom.com/wiki/Habitica_Wiki"
|
||||
target="_blank"
|
||||
>{{ $t('wiki') }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
v-once
|
||||
href="https://tools.habitica.com/"
|
||||
target="_blank"
|
||||
>{{ $t('dataDisplayTool') }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
{{ $t('reportBug') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
v-once
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
|
||||
target="_blank"
|
||||
>{{ $t('requestFeature') }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a"
|
||||
>
|
||||
{{ $t('askQuestionGuild') }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</sidebar-section>
|
||||
<sidebar-section :title="$t('playerTiers')">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p v-once>
|
||||
{{ $t('playerTiersDesc') }}
|
||||
</p>
|
||||
<ul class="tier-list">
|
||||
<li
|
||||
v-once
|
||||
class="tier1"
|
||||
>
|
||||
{{ $t('tier1') }}
|
||||
<div
|
||||
class="svg-icon tier1-icon"
|
||||
v-html="icons.tier1"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier2"
|
||||
>
|
||||
{{ $t('tier2') }}
|
||||
<div
|
||||
class="svg-icon tier2-icon"
|
||||
v-html="icons.tier2"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier3"
|
||||
>
|
||||
{{ $t('tier3') }}
|
||||
<div
|
||||
class="svg-icon tier3-icon"
|
||||
v-html="icons.tier3"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier4"
|
||||
>
|
||||
{{ $t('tier4') }}
|
||||
<div
|
||||
class="svg-icon tier4-icon"
|
||||
v-html="icons.tier4"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier5"
|
||||
>
|
||||
{{ $t('tier5') }}
|
||||
<div
|
||||
class="svg-icon tier5-icon"
|
||||
v-html="icons.tier5"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier6"
|
||||
>
|
||||
{{ $t('tier6') }}
|
||||
<div
|
||||
class="svg-icon tier6-icon"
|
||||
v-html="icons.tier6"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="tier7"
|
||||
>
|
||||
{{ $t('tier7') }}
|
||||
<div
|
||||
class="svg-icon tier7-icon"
|
||||
v-html="icons.tier7"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="moderator"
|
||||
>
|
||||
{{ $t('tierModerator') }}
|
||||
<div
|
||||
class="svg-icon mod-icon"
|
||||
v-html="icons.tierMod"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="staff"
|
||||
>
|
||||
{{ $t('tierStaff') }}
|
||||
<div
|
||||
class="svg-icon staff-icon"
|
||||
v-html="icons.tierStaff"
|
||||
></div>
|
||||
</li>
|
||||
<li
|
||||
v-once
|
||||
class="npc"
|
||||
>
|
||||
{{ $t('tierNPC') }}
|
||||
<div
|
||||
class="svg-icon npc-icon"
|
||||
v-html="icons.tierNPC"
|
||||
></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</sidebar-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
h1 {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: $gray-600;
|
||||
padding: 0em;
|
||||
}
|
||||
|
||||
.pause-button {
|
||||
background-color: #ffb445 !important;
|
||||
color: $white;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grassy-meadow-backdrop {
|
||||
background-repeat: repeat-x;
|
||||
width: 100%;
|
||||
height: 246px;
|
||||
}
|
||||
|
||||
.daniel_front {
|
||||
height: 246px;
|
||||
width: 471px;
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.tier1-icon, .tier2-icon {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.tier5-icon, .tier6-icon {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.tier7-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.mod-icon {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.npc-icon {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.boss-icon {
|
||||
width: 16px;
|
||||
margin-top: .1em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.boss-icon-large {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.staff {
|
||||
margin-bottom: 1em;
|
||||
|
||||
.staff-icon {
|
||||
width: 11px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #6133b4;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.tier-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 98%;
|
||||
|
||||
li {
|
||||
border-radius: 2px;
|
||||
background-color: #edecee;
|
||||
border: solid 1px #c3c0c7;
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tier1 {
|
||||
color: #c42870;
|
||||
}
|
||||
|
||||
.tier2 {
|
||||
color: #b01515;
|
||||
}
|
||||
|
||||
.tier3 {
|
||||
color: #d70e14;
|
||||
}
|
||||
|
||||
.tier4 {
|
||||
color: #c24d00;
|
||||
}
|
||||
|
||||
.tier5 {
|
||||
color: #9e650f;
|
||||
}
|
||||
|
||||
.tier6 {
|
||||
color: #2b8363;
|
||||
}
|
||||
|
||||
.tier7 {
|
||||
color: #167e87;
|
||||
}
|
||||
|
||||
.tier8, .moderator {
|
||||
color: #277eab;
|
||||
}
|
||||
|
||||
.tier9, .staff {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.npc {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.staff .title {
|
||||
color: #6133b4;
|
||||
}
|
||||
|
||||
.moderator .title {
|
||||
color: #277eab;
|
||||
}
|
||||
|
||||
.bailey .title {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.boss-section {
|
||||
padding: 1.75em;
|
||||
}
|
||||
|
||||
.world-boss {
|
||||
color: $white;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
margin: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quest-boss {
|
||||
margin: 1em auto;
|
||||
}
|
||||
|
||||
.grey-progress-bar {
|
||||
width: 100%;
|
||||
height: 15px;
|
||||
background-color: rgba(255, 255, 255, 0.24);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.boss-health-bar {
|
||||
width: 80%;
|
||||
height: 15px;
|
||||
margin-bottom: .5em;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
background-color: #f74e52;
|
||||
}
|
||||
|
||||
.boss-health-bar.rage-bar {
|
||||
background-color: #ff944c;
|
||||
}
|
||||
|
||||
.boss-gradient {
|
||||
background-image: linear-gradient(to bottom, #401f2a, #931f4d);
|
||||
margin-top: -1.4em;
|
||||
}
|
||||
|
||||
.boss-description {
|
||||
border-top: 1px solid;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
padding: .25em 0 0 .25em;
|
||||
}
|
||||
|
||||
.float-bar {
|
||||
position: relative;
|
||||
top: -16px;
|
||||
width: 162px;
|
||||
height: 28px;
|
||||
border-radius: 2px;
|
||||
background-color: inherit;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.corner-decoration {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: inherit;
|
||||
border: inherit;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.reverse {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.diamond {
|
||||
margin: auto;
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
background-color: #dc4069;
|
||||
border: solid 2px #931f4d;
|
||||
}
|
||||
|
||||
.reduce {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rage-strike {
|
||||
max-width: 50px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.rage-strike-active {
|
||||
max-width: 75px;
|
||||
height: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.world-boss-info-button {
|
||||
width: 100%;
|
||||
background-color: $gray-500;
|
||||
border-radius: 2px;
|
||||
font-size: 14px;
|
||||
color: $blue-10;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import { TAVERN_ID } from '@/../../common/script/constants';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { goToModForm } from '@/libs/modform';
|
||||
|
||||
import worldBossInfoModal from '../world-boss/worldBossInfoModal';
|
||||
import worldBossRageModal from '../world-boss/worldBossRageModal';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import chat from './chat';
|
||||
|
||||
import challengeIcon from '@/assets/svg/challenge.svg?raw';
|
||||
import chevronIcon from '@/assets/svg/chevron-red.svg?raw';
|
||||
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||
import healthIcon from '@/assets/svg/health.svg?raw';
|
||||
import informationIconRed from '@/assets/svg/information-red.svg?raw';
|
||||
import questBackground from '@/assets/svg/quest-background-border.svg?raw';
|
||||
import rageIcon from '@/assets/svg/rage.svg?raw';
|
||||
import swordIcon from '@/assets/svg/sword.svg?raw';
|
||||
|
||||
import tier1 from '@/assets/svg/tier-1.svg?raw';
|
||||
import tier2 from '@/assets/svg/tier-2.svg?raw';
|
||||
import tier3 from '@/assets/svg/tier-3.svg?raw';
|
||||
import tier4 from '@/assets/svg/tier-4.svg?raw';
|
||||
import tier5 from '@/assets/svg/tier-5.svg?raw';
|
||||
import tier6 from '@/assets/svg/tier-6.svg?raw';
|
||||
import tier7 from '@/assets/svg/tier-7.svg?raw';
|
||||
import tierMod from '@/assets/svg/tier-mod.svg?raw';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg?raw';
|
||||
import tierStaff from '@/assets/svg/tier-staff.svg?raw';
|
||||
|
||||
import staffList from '../../libs/staffList';
|
||||
import reportBug from '@/mixins/reportBug.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
worldBossInfoModal,
|
||||
worldBossRageModal,
|
||||
sidebarSection,
|
||||
chat,
|
||||
},
|
||||
mixins: [reportBug],
|
||||
data () {
|
||||
return {
|
||||
groupId: TAVERN_ID,
|
||||
icons: Object.freeze({
|
||||
challengeIcon,
|
||||
chevronIcon,
|
||||
gem: gemIcon,
|
||||
healthIcon,
|
||||
informationIcon: informationIconRed,
|
||||
questBackground,
|
||||
rageIcon,
|
||||
swordIcon,
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tierMod,
|
||||
tierNPC,
|
||||
tierStaff,
|
||||
}),
|
||||
group: {
|
||||
chat: [],
|
||||
},
|
||||
sections: {
|
||||
worldBoss: true,
|
||||
},
|
||||
staff: staffList,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
questData () {
|
||||
if (!this.group.quest) return {};
|
||||
return quests.quests[this.group.quest.key];
|
||||
},
|
||||
imageURLs () {
|
||||
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
|
||||
if (!currentEvent) {
|
||||
return {
|
||||
background: 'url(/static/npc/normal/tavern_background.png)',
|
||||
npc: 'url(/static/npc/normal/tavern_npc.png)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: `url(/static/npc/${currentEvent.season}/tavern_background.png)`,
|
||||
npc: `url(/static/npc/${currentEvent.season}/tavern_npc.png)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
subSection: this.$t('tavern'),
|
||||
section: this.$t('guilds'),
|
||||
});
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', { groupId: TAVERN_ID });
|
||||
},
|
||||
methods: {
|
||||
modForm () {
|
||||
goToModForm(this.user);
|
||||
},
|
||||
toggleSleep () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
|
||||
pendingDamage () {
|
||||
if (!this.user.party.quest.progress.up) return 0;
|
||||
return this.$options.filters.floor(this.user.party.quest.progress.up, 10);
|
||||
// keep user's pending damage consistent with how it's displayed on the party page
|
||||
},
|
||||
bossCurrentHealth () {
|
||||
if (!this.group.quest.progress.hp) return 0;
|
||||
|
||||
return Math.ceil(parseFloat(this.group.quest.progress.hp)).toLocaleString();
|
||||
},
|
||||
bossCurrentRage () {
|
||||
if (!this.group.quest.progress.hp) return 0;
|
||||
|
||||
return Math.floor(parseFloat(this.group.quest.progress.rage)).toLocaleString();
|
||||
},
|
||||
showWorldBossInfo () {
|
||||
this.$root.$emit('bv::show::modal', 'world-boss-info');
|
||||
},
|
||||
showWorldBossRage (npc) {
|
||||
if (this.group.quest.extra.worldDmg[npc]) {
|
||||
this.$store.state.rageModalOptions.npc = npc;
|
||||
this.$root.$emit('bv::show::modal', 'world-boss-rage');
|
||||
}
|
||||
},
|
||||
async fetchRecentMessages () {
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', { groupId: TAVERN_ID });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="row standard-page">
|
||||
<small
|
||||
class="muted"
|
||||
v-html="$t('blurbHallContributors')"
|
||||
v-html="$t('blurbHallContributors', hallLinks)"
|
||||
></small>
|
||||
</div>
|
||||
<div class="row standard-page">
|
||||
@@ -68,6 +68,7 @@
|
||||
<a
|
||||
href="https://habitica.fandom.com/wiki/Contributor_Rewards"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>More details</a>
|
||||
</small>
|
||||
</div>
|
||||
@@ -296,6 +297,7 @@
|
||||
<div
|
||||
v-markdown="hero.contributor.contributions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
></div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -360,6 +362,12 @@ export default {
|
||||
expandItems: false,
|
||||
expandAuth: false,
|
||||
expandTransactions: false,
|
||||
hallLinks: {
|
||||
linkRewards: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tier-rewards" target="_blank" rel="noreferrer noopener">',
|
||||
linkTiers: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tiers" target="_blank" rel="noreferrer noopener">',
|
||||
linkContributing: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
async mounted () {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<base-banner
|
||||
v-if="showChatWarning"
|
||||
banner-id="chat-warning"
|
||||
banner-class="chat-banner"
|
||||
class="chat-banner"
|
||||
height="3rem"
|
||||
:class="{faq: faqPage}"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="w-100 text-center"
|
||||
v-html="$t('chatSunsetWarning')"
|
||||
>
|
||||
</div>
|
||||
</base-banner>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.chat-banner {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 8px;
|
||||
color: $orange-1;
|
||||
background-color: $orange-100;
|
||||
line-height: 1.71;
|
||||
|
||||
a {
|
||||
color: $orange-1;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: $orange-1;
|
||||
}
|
||||
}
|
||||
|
||||
&.faq {
|
||||
position: fixed;
|
||||
top: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import BaseBanner from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseBanner,
|
||||
},
|
||||
computed: {
|
||||
faqPage () {
|
||||
return (this.$route.fullPath.indexOf('/faq')) !== -1;
|
||||
},
|
||||
showChatWarning () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<p
|
||||
class="mr-3 mb-0"
|
||||
v-html="$t('privacyOverview') + ' ' + $t('learnMorePrivacy')"
|
||||
v-html="$t('privacyOverview') + ' ' + $t('learnMorePrivacy', learnLinks)"
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
@@ -89,6 +89,10 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
learnLinks: {
|
||||
linkOpen: '<a href="/static/privacy" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
|
||||
@@ -123,7 +123,6 @@
|
||||
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapGetters, mapActions } from '@/libs/store';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
@@ -236,22 +235,8 @@ export default {
|
||||
},
|
||||
async createOrInviteParty () {
|
||||
if (this.user.party._id) {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Find Party Members',
|
||||
});
|
||||
this.$router.push('/looking-for-party');
|
||||
} else {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Get Started',
|
||||
});
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -310,6 +310,7 @@
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
{{ $t('reportBug') }}
|
||||
@@ -317,6 +318,7 @@
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="openBugReportModal(true)"
|
||||
>
|
||||
{{ $t('askQuestion') }}
|
||||
@@ -325,6 +327,7 @@
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('requestFeature') }}</a>
|
||||
</div>
|
||||
</li>
|
||||
@@ -377,6 +380,7 @@
|
||||
v-if="hasPermission(user, 'news')"
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://panel.habitica.com"
|
||||
>
|
||||
{{ $t('newsroom') }}
|
||||
|
||||
@@ -6,11 +6,7 @@
|
||||
>
|
||||
<div slot="content">
|
||||
<div
|
||||
v-html="$t('invitedToPartyBy', {
|
||||
userId: notification.data.inviter,
|
||||
userName: invitingUser.auth ? invitingUser.auth.local.username : null,
|
||||
party: notification.data.name,
|
||||
})"
|
||||
v-html="$t('invitedToPartyBy', invitationInfo)"
|
||||
>
|
||||
</div>
|
||||
<div class="notifications-buttons">
|
||||
@@ -60,6 +56,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
invitationInfo: {
|
||||
usernameLink: `<a href="/profile/${this.notification.data.inviter}" target="_blank" rel="noreferrer noopener">@${this.invitingUser.auth ? this.invitingUser.auth.local.username : null}</a>`,
|
||||
partyName: `<span class="notification-bold">${this.notification.data.name}</span>`,
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.invitingUser = await this.$store.dispatch('members:fetchMember', {
|
||||
|
||||
@@ -139,8 +139,8 @@ export default {
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
|
||||
@@ -114,7 +114,6 @@ import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import guide from '@/mixins/guide';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import yesterdailyModal from './tasks/yesterdailyModal';
|
||||
import newStuff from './news/modal';
|
||||
@@ -648,15 +647,6 @@ export default {
|
||||
// Reset daily analytics actions
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
|
||||
} else {
|
||||
// Note a failed cron event, for our records and investigation
|
||||
Analytics.track({
|
||||
eventName: 'cron failed',
|
||||
eventAction: 'cron failed',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
responseCode: response.status,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Sync
|
||||
|
||||
@@ -197,7 +197,7 @@ export default {
|
||||
},
|
||||
amazonPayments: {},
|
||||
assistanceEmailObject: {
|
||||
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
|
||||
techAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
|
||||
},
|
||||
sendingInProgress: false,
|
||||
userReceivingGems: null,
|
||||
|
||||
@@ -433,9 +433,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
|
||||
// analytics
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectTranslatedArray,
|
||||
@@ -536,16 +533,6 @@ export default {
|
||||
this.close();
|
||||
},
|
||||
submit () {
|
||||
if (this.paymentData.group && !this.paymentData.newGroup) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan upgrade',
|
||||
eventAction: 'group plan upgrade',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.upgradedGroup.demographics,
|
||||
type: this.paymentData.group.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
this.paymentData = {};
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
},
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<a
|
||||
href="/static/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{{ $t('habiticaPrivacyPolicy') }}
|
||||
</a>
|
||||
|
||||
@@ -399,7 +399,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="!hasGroupPlan && !canCancelSubscription"
|
||||
v-html="$t(`cancelSubInfo${user.purchased.plan.paymentMethod}`)"
|
||||
v-html="$t(`cancelSubInfo${user.purchased.plan.paymentMethod}`, cancelLinks)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1022,6 +1022,17 @@ export default {
|
||||
|
||||
return nextHourglassMonth;
|
||||
},
|
||||
cancelLinks () {
|
||||
const links = {
|
||||
linkClose: '</a>',
|
||||
};
|
||||
if (this.user.purchased.plan.paymentMethod === this.paymentMethods.GOOGLE) {
|
||||
links.linkOpen = '<a href="https://play.google.com/store/account/subscriptions" target="_blank" rel="noopener noreferrer">';
|
||||
} else if (this.user.purchased.plan.paymentMethod === this.paymentMethods.APPLE) {
|
||||
links.linkOpen = '<a href="https://support.apple.com/en-us/HT202039" target="_blank" rel="noopener noreferrer">';
|
||||
}
|
||||
return links;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</h2>
|
||||
<p
|
||||
class="text-center"
|
||||
v-html="$t('usernameInfo')"
|
||||
v-html="$t('usernameInfo', usernameLinks)"
|
||||
></p>
|
||||
<username-form />
|
||||
<div class="scene_veteran_pets center-block"></div>
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="small text-center tos-footer"
|
||||
v-html="$t('usernameTOSRequirements')"
|
||||
v-html="$t('usernameTOSRequirements', requirementsLinks)"
|
||||
></div>
|
||||
</b-modal>
|
||||
</template>
|
||||
@@ -94,6 +94,15 @@ export default {
|
||||
icons: Object.freeze({
|
||||
helloNametag,
|
||||
}),
|
||||
usernameLinks: {
|
||||
linkOpen: '<a href="https://habitica.fandom.com/wiki/Player_Names" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
requirementsLinks: {
|
||||
termsLink: '<a href="/static/terms" target="_blank" rel="noreferrer noopener">',
|
||||
guidelinesLink: '<a href="/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
<div
|
||||
class="contact mx-auto"
|
||||
>
|
||||
<p v-html="$t('contactAdmin')"></p>
|
||||
<p v-html="$t('contactAdmin', { adminEmail })"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -368,6 +368,7 @@ export default {
|
||||
pixel_border: 'url(/static/npc/normal/pixel_border.png)',
|
||||
},
|
||||
staff: staffList,
|
||||
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,8 +33,9 @@
|
||||
class="drawer-help-text"
|
||||
href="/static/faq#pet-foods"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>{{ $t('petLikeToEat') }}</span>
|
||||
<span>{{ $t('petLikeToEat', eatLinks) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</drawer-header-tabs>
|
||||
@@ -114,6 +115,10 @@ export default {
|
||||
},
|
||||
],
|
||||
selectedDrawerTab: this.defaultSelectedTab,
|
||||
eatLinks: {
|
||||
linkOpen: '<a href="/static/faq#pet-foods" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -162,6 +162,10 @@ export default {
|
||||
searchTextThrottled: null,
|
||||
unfilteredCategories: [],
|
||||
viewOptions: {},
|
||||
customizeLinks: {
|
||||
linkOpen: '<a href="">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -264,21 +268,21 @@ export default {
|
||||
const { $t } = this;
|
||||
switch (identifier) {
|
||||
case 'animalEars':
|
||||
return $t('allCustomizationsOwned');
|
||||
return $t('allCustomizationsOwned', this.customizeLinks);
|
||||
case 'animalTails':
|
||||
return $t('allCustomizationsOwned');
|
||||
return $t('allCustomizationsOwned', this.customizeLinks);
|
||||
case 'backgrounds':
|
||||
return `${$t('allCustomizationsOwned')} ${$t('checkNextMonth')}`;
|
||||
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextMonth')}`;
|
||||
case 'facialHair':
|
||||
return $t('allCustomizationsOwned');
|
||||
return $t('allCustomizationsOwned', this.customizeLinks);
|
||||
case 'color':
|
||||
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
|
||||
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextSeason')}`;
|
||||
case 'base':
|
||||
return $t('allCustomizationsOwned');
|
||||
return $t('allCustomizationsOwned', this.customizeLinks);
|
||||
case 'shirt':
|
||||
return $t('allCustomizationsOwned');
|
||||
return $t('allCustomizationsOwned', this.customizeLinks);
|
||||
case 'skin':
|
||||
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
|
||||
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextSeason')}`;
|
||||
default:
|
||||
return `Unknown identifier ${identifier}`;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
:item-margin="24"
|
||||
:type="category.identifier"
|
||||
:fold-button="false"
|
||||
:no-items-label="$t('allEquipmentOwned')"
|
||||
:no-items-label="$t('allEquipmentOwned', equipmentLinks)"
|
||||
:click-handler="false"
|
||||
>
|
||||
<template
|
||||
@@ -229,29 +229,25 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
viewOptions: {},
|
||||
|
||||
searchText: null,
|
||||
searchTextThrottled: null,
|
||||
|
||||
icons: Object.freeze({
|
||||
hourglass: svgHourglass,
|
||||
}),
|
||||
|
||||
sortItemsBy: ['AZ', 'sortByNumber'],
|
||||
selectedSortItemsBy: 'AZ',
|
||||
|
||||
selectedItemToBuy: null,
|
||||
|
||||
hidePinned: false,
|
||||
|
||||
backgroundUpdate: new Date(),
|
||||
|
||||
currentEvent: null,
|
||||
|
||||
imageURLs: {
|
||||
background: '',
|
||||
npc: '',
|
||||
},
|
||||
equipmentLinks: {
|
||||
linkOpen: '<a href="/inventory/equipment">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -37,6 +37,9 @@ export default {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
if (response.email) {
|
||||
window.sessionStorage.setItem('apple-email', response.email);
|
||||
}
|
||||
window.location.href = '/username';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="body-text">
|
||||
<p v-html="$t('sunsetFaqPara12')"></p>
|
||||
<p v-html="$t('sunsetFaqPara13')"></p>
|
||||
<p v-html="$t('sunsetFaqPara14')"></p>
|
||||
<p v-html="$t('sunsetFaqPara14', translationLinks)"></p>
|
||||
<p v-html="$t('sunsetFaqPara15')"></p>
|
||||
<p v-html="$t('sunsetFaqPara16')"></p>
|
||||
<p v-html="$t('sunsetFaqPara17')"></p>
|
||||
@@ -138,9 +138,9 @@
|
||||
</div>
|
||||
<div class="body-text">
|
||||
<ul>
|
||||
<li v-html="$t('sunsetFaqList8')"></li>
|
||||
<li v-html="$t('sunsetFaqList9')"></li>
|
||||
<li v-html="$t('sunsetFaqList10')"></li>
|
||||
<li v-html="$t('sunsetFaqList8', faqLinks)"></li>
|
||||
<li v-html="$t('sunsetFaqList9', beginnerLinks)"></li>
|
||||
<li v-html="$t('sunsetFaqList10', { adminEmail })"></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -169,6 +169,23 @@ export default {
|
||||
components: {
|
||||
FaqSidebar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
|
||||
translationLinks: {
|
||||
linkOpen: '<a href="https://translate.habitica.com/projects/habitica/#information">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
faqLinks: {
|
||||
linkOpen: '<a href="https://habitica.com/static/faq">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
beginnerLinks: {
|
||||
linkOpen: '<a href="https://habitica.wordpress.com/beginning-adventurers-guide/">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('sunsetFaqTitle'),
|
||||
|
||||
@@ -33,11 +33,11 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
localStorageTryFirst: {
|
||||
linkStart: '<a href="/user/settings/site" target="_blank">',
|
||||
linkStart: '<a href="/user/settings/site" target="_blank" rel="noopener noreferrer">',
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
localStorageTryNext: {
|
||||
linkStart: '<a href="https://github.com/HabitRPG/habitica/issues/2760" target="_blank">',
|
||||
linkStart: '<a href="https://github.com/HabitRPG/habitica/issues/2760" target="_blank" rel="noopener noreferrer">',
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
<ul>
|
||||
<li><strong>{{ $t('commGuideList01A') }}</strong></li>
|
||||
<li v-html="$t('commGuideList02C')"></li>
|
||||
<li v-html="$t('commGuideList02N')"></li>
|
||||
<li v-html="$t('commGuideList02N', { adminEmail })"></li>
|
||||
<li v-html="$t('commGuideList02H')"></li>
|
||||
<li v-html="$t('commGuideList02A')"></li>
|
||||
<li v-html="$t('commGuideList02I')"></li>
|
||||
<li v-html="$t('commGuideList02G')"></li>
|
||||
<li v-html="$t('commGuideList02G', { adminEmail })"></li>
|
||||
<li v-html="$t('commGuideList02D')"></li>
|
||||
<li v-html="$t('commGuideList02E')"></li>
|
||||
<li v-html="$t('commGuideList02O')"></li>
|
||||
@@ -36,7 +36,7 @@
|
||||
<li v-html="$t('commGuideList02P')"></li>
|
||||
<li v-html="$t('commGuideList02Q')"></li>
|
||||
<li v-html="$t('commGuideList02M')"></li>
|
||||
<li v-html="$t('commGuideList02L')"></li>
|
||||
<li v-html="$t('commGuideList02L', { adminEmail })"></li>
|
||||
<li v-html="$t('commGuideList02J')"></li>
|
||||
<li v-html="$t('commGuideList02K')"></li>
|
||||
</ul>
|
||||
@@ -69,7 +69,7 @@
|
||||
<p v-html="$t('commGuidePara054')"></p>
|
||||
<p v-html="$t('commGuidePara055')"></p>
|
||||
<ul>
|
||||
<li v-html="$t('commGuideList06A')"></li>
|
||||
<li v-html="$t('commGuideList06A', { adminEmail })"></li>
|
||||
<li v-html="$t('commGuideList06C')"></li>
|
||||
<li v-html="$t('commGuideList06E')"></li>
|
||||
</ul>
|
||||
@@ -108,7 +108,7 @@
|
||||
class="mb-3"
|
||||
>
|
||||
<p v-html="$t('commGuidePara061')"></p>
|
||||
<p v-html="$t('commGuidePara063')"></p>
|
||||
<p v-html="$t('commGuidePara063', { adminEmail })"></p>
|
||||
<h2 id="meet-the-mods">
|
||||
{{ $t('commGuideHeadingMeet') }}
|
||||
</h2>
|
||||
@@ -162,15 +162,15 @@
|
||||
<h2 id="final">
|
||||
{{ $t('commGuideHeadingFinal') }}
|
||||
</h2>
|
||||
<p v-html="$t('commGuidePara067')"></p>
|
||||
<p v-html="$t('commGuidePara067', { adminEmail })"></p>
|
||||
<p v-html="$t('commGuidePara068')"></p>
|
||||
<h2 id="links">
|
||||
{{ $t('commGuideHeadingLinks') }}
|
||||
</h2>
|
||||
<ul>
|
||||
<li><a href="/static/faq">{{ $t('faq') }}</a></li>
|
||||
<li v-html="$t('commGuideLink03')"></li>
|
||||
<li v-html="$t('commGuideLink04')"></li>
|
||||
<li v-html="$t('commGuideLink03', gitHubLinks)"></li>
|
||||
<li v-html="$t('commGuideLink04', feedbackLinks)"></li>
|
||||
</ul>
|
||||
<p v-html="$t('commGuidePara069')"></p>
|
||||
<ul>
|
||||
@@ -187,3 +187,23 @@
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
|
||||
gitHubLinks: {
|
||||
linkOpen: '<a href="https://github.com/HabitRPG/habitica" target="_blank">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
feedbackLinks: {
|
||||
linkOpen: '<a href="https://habitica.fandom.com/wiki/Habitica_Wiki" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<br>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
{{ $t('reportBug') }}
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
</h2>
|
||||
<p
|
||||
class="purple-600"
|
||||
v-html="$t('checkGroupPlanFAQ')"
|
||||
v-html="$t('checkGroupPlanFAQ', faqLinks)"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -329,6 +329,10 @@ export default {
|
||||
modalOption: '',
|
||||
modalPage: 'account',
|
||||
modalTitle: this.$t('register'),
|
||||
faqLinks: {
|
||||
linkOpen: '<a href="/static/faq#what-is-group-plan">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
class="nav-link"
|
||||
href="https://habitica.wordpress.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('companyBlog') }}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
@@ -54,6 +55,7 @@
|
||||
class="nav-link"
|
||||
href="https://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('tumblr') }}</a>
|
||||
</li>
|
||||
<router-link
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
<noscript class="banner">
|
||||
{{ $t('jsDisabledHeadingFull') }}
|
||||
<br />
|
||||
<a href="https://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
|
||||
<a
|
||||
href="https://www.enable-javascript.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ $t('jsDisabledLink') }}</a>
|
||||
</noscript>
|
||||
<privacy-banner
|
||||
class="privacy-banner"
|
||||
@@ -254,12 +258,14 @@
|
||||
class="app svg-icon"
|
||||
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
v-html="icons.googlePlay"
|
||||
></a>
|
||||
<a
|
||||
class="app svg-icon"
|
||||
href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
v-html="icons.iosAppStore"
|
||||
></a>
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
<hr>
|
||||
</div>
|
||||
<p>
|
||||
<span v-html="$t('overviewQuestionsRevised')"></span>
|
||||
<span v-html="$t('overviewQuestionsRevised', overviewLinks)"></span>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click.prevent="openBugReportModal(true)"
|
||||
>
|
||||
{{ $t('askQuestion') }}
|
||||
@@ -57,6 +58,10 @@ export default {
|
||||
shopUrl: '/shops/market',
|
||||
},
|
||||
},
|
||||
overviewLinks: {
|
||||
linkOpen: '<a href="/static/faq">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -100,9 +100,11 @@
|
||||
We use Google Analytics, a service which uses cookies to collect and analyze data about the use of the Services and report on activities and trends. This service may also collect data about the use of other websites, apps, and online services. You can <a
|
||||
href="https://policies.google.com/technologies/partner-sites"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>learn about</a> Google's practices, and opt out of them, by downloading the <a
|
||||
href="https://tools.google.com/dlpage/gaoptout"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Google Analytics opt-out browser add-on</a>.
|
||||
</p>
|
||||
<h4>Controlling Cookies</h4>
|
||||
@@ -687,6 +689,7 @@
|
||||
<em>Opt - Out of the “ Sale” of Personal Information or Use of Such Information for “Targeted Advertising” or “Profiling”</em>. We engage in common marketing and advertising practices to provide more relevant content and ads to users of our Site and Services. Certain of these practices may involve the “selling” of personal information, or the use of such information for “targeted advertising” or “profiling,” as those terms are defined in the Texas Data Privacy and Security Act (“TDPSA”) and the Nebraska Data Privacy Act. We do not sell personal information under the more commonly understood meaning of that word—i.e., providing personal information to third parties in exchange for money. Nor do we have actual knowledge of selling personal information of minors under the age of 16. To opt-out of the selling of your personal information, or use of that information for targeted advertising or profiling, please submit a request to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. Note: We also treat Global Privacy Control browser signals as opt-out of sale/disclosure for targeted advertising or profiling requests. To opt-out via the Global Privacy Control, please follow the instructions available <a
|
||||
href="https://globalprivacycontrol.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>here</a>.
|
||||
</p>
|
||||
<p>
|
||||
@@ -730,12 +733,15 @@
|
||||
<strong>Right to lodge a complaint:</strong> Users that reside in the UK, EEA, or Switzerland have the right to seek information and assistance or lodge a complaint about our data collection and processing actions with the supervisory authority where they reside. Contact details for data protection authorities are available here. UK: <a
|
||||
href="https://ico.org.uk/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>https://ico.org.uk/</a> EEA: <a
|
||||
href="https://edpb.europa.eu/about-edpb/board/members_en"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>https://edpb.europa.eu/about-edpb/board/members_en</a> Switzerland: <a
|
||||
href="https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html</a>.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -244,13 +244,11 @@
|
||||
|
||||
<script>
|
||||
import AppFooter from '@/components/appFooter';
|
||||
import ChatBanner from '@/components/header/banners/chatBanner';
|
||||
import StaticHeader from './header.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppFooter,
|
||||
ChatBanner,
|
||||
StaticHeader,
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
Our Service is provided by HabitRPG, Inc. ("HabitRPG"). By accepting these Terms of Service and our Privacy Policy located at: <a
|
||||
href="https://habitica.com/static/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>https://habitica.com/static/privacy</a> (collectively, the "Agreement"), using our website, Habitica.com, or our other features or services (collectively, “the Services”), or otherwise manifesting your assent to the Agreement, you acknowledge that you have read, understood, and agree to be legally bound by the Agreement. If you do not agree to (or cannot comply with) the Agreement, you are not permitted to access or use the Service. By accepting or agreeing to this Agreement on behalf of a company or other legal entity, you represent and warrant that you have the authority to bind that company or other legal entity to the Agreement and, in such event, "you" and "your" will refer and apply to that company or other legal entity. You further represent and warrant that your assent to this Agreement constitutes an electronic signature as defined by the Electronic Signatures in Global and National Commerce Act (“E-Sign”) and the Uniform Electronic Transactions Act (“UETA”) and that you have formed, executed, entered into, and accepted the terms of and otherwise authenticated the Agreement and acknowledged and agreed that the Agreement is an electronic record for purposes of E- Sign, UETA, and the Uniform Computer Information Transactions Act and, as such, is completely valid, has legal effect, is enforceable, and is binding on, and non- refutable by, you and/or any entity on whose behalf you are acting.
|
||||
</p>
|
||||
<p class="strong">
|
||||
@@ -164,6 +165,7 @@
|
||||
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
|
||||
href="https://reportaproblem.apple.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUNDS FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
</p>
|
||||
|
||||
@@ -204,6 +206,7 @@
|
||||
All disputes will be resolved before a neutral arbitrator selected jointly by the parties, whose decision will be final, except for a limited right of appeal under the FAA. The arbitration shall be commenced and conducted by JAMS pursuant to its then current Comprehensive Arbitration Rules and Procedures and in accordance with the Expedited Procedures in those rules, or, where appropriate, pursuant to JAMS' Streamlined Arbitration Rules and Procedures. All applicable JAMS rules and procedures are available at the JAMS website <a
|
||||
href="https://www.jamsadr.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>www.jamsadr.com</a>. Each party will be responsible for paying any JAMS filing, administrative, and arbitrator fees in accordance with JAMS rules. Judgment on the arbitrator's award may be entered in any court having jurisdiction. This clause shall not preclude parties from seeking provisional remedies in aid of arbitration from a court of appropriate jurisdiction. The arbitration may be conducted in person, through the submission of documents, by phone, or online. If conducted in person, the arbitration shall take place in the United States county where you reside. The parties may litigate in court to compel arbitration, to stay a proceeding pending arbitration, or to confirm, modify, vacate, or enter judgment on the award entered by the arbitrator. The parties shall cooperate in good faith in the voluntary and informal exchange of all non-privileged documents and other information (including electronically stored information) relevant to the Dispute immediately after commencement of the arbitration. As set forth below, nothing in this Agreement will prevent us from seeking injunctive relief in any court of competent jurisdiction as necessary to protect our proprietary interests.
|
||||
</p>
|
||||
<p>
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="taskList.length > 0"
|
||||
v-if="taskList.length > 0 && !rerendering"
|
||||
ref="tasksList"
|
||||
class="sortable-tasks"
|
||||
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
|
||||
@@ -432,6 +432,7 @@ export default {
|
||||
|
||||
selectedItemToBuy: {},
|
||||
dragging: false,
|
||||
rerendering: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -548,8 +549,8 @@ export default {
|
||||
if (this.taskListOverride) originTasks = this.taskListOverride;
|
||||
|
||||
// Server
|
||||
const taskIdToReplace = filteredList[data.newIndex];
|
||||
const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
|
||||
const taskIdToReplace = filteredList[data.newIndex]._id;
|
||||
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace);
|
||||
|
||||
let newOrder;
|
||||
if (taskToMove.group.id && !this.isUser) {
|
||||
@@ -568,6 +569,9 @@ export default {
|
||||
// Client
|
||||
const deleted = originTasks.splice(data.oldIndex, 1);
|
||||
originTasks.splice(data.newIndex, 0, deleted[0]);
|
||||
this.rerendering = true;
|
||||
await this.$nextTick();
|
||||
this.rerendering = false;
|
||||
},
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
}, `type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
tabindex="0"
|
||||
@keypress.enter="$emit('editTask', task)"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
@@ -98,9 +100,7 @@
|
||||
<div
|
||||
class="task-clickable-area pt-1 pl-75 pb-0"
|
||||
:class="{ 'cursor-auto': !teamManagerAccess }"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3
|
||||
@@ -432,10 +432,6 @@
|
||||
outline: none;
|
||||
transition: none;
|
||||
border: $purple-400 solid 1px;
|
||||
|
||||
:not(task-best-control-inner-habit) { // round icon
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-bottom-box {
|
||||
@@ -462,16 +458,13 @@
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.task:not(.groupTask) {
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.left-control, .right-control, .task-content {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
&:hover, &:focus {
|
||||
border: none;
|
||||
outline: 1px solid $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,11 +515,6 @@
|
||||
&-user {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: 4px;
|
||||
border: $purple-400 solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-title + .task-dropdown ::v-deep .dropdown-menu {
|
||||
|
||||
@@ -55,11 +55,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<lockable-label
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<div class="d-flex align-items-center">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
:locked="challengeAccessRequired"
|
||||
:text="`${$t('text')}*`"
|
||||
/>
|
||||
<div
|
||||
id="spi-alert"
|
||||
class="d-flex align-items-center"
|
||||
:class="cssClass('headings')"
|
||||
>
|
||||
<div
|
||||
class="svg svg-icon color icon-16 mr-1"
|
||||
v-html="icons.alert"
|
||||
></div>
|
||||
<small
|
||||
class="my-1"
|
||||
>
|
||||
<a
|
||||
:class="cssClass('headings')"
|
||||
>{{ $t('avoidSPI') }}</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
ref="inputToFocus"
|
||||
v-model="task.text"
|
||||
@@ -79,10 +99,20 @@
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<b-popover
|
||||
:target="'spi-alert'"
|
||||
triggers="hover"
|
||||
placement="bottom"
|
||||
offset="-128"
|
||||
>
|
||||
<div
|
||||
v-html="$t('avoidSPIDetails', spiLinkData)">
|
||||
</div>
|
||||
</b-popover>
|
||||
<div
|
||||
class="form-group mb-0"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex align-items-center">
|
||||
<lockable-label
|
||||
class="mr-auto"
|
||||
:class-override="cssClass('headings')"
|
||||
@@ -382,6 +412,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="task.type === 'daily' && schedulingSummary"
|
||||
class="scheduling-summary mt-2 mb-0"
|
||||
>
|
||||
{{ schedulingSummary }}
|
||||
</p>
|
||||
<div
|
||||
v-if="task.type === 'daily' && schedulingWarning"
|
||||
class="scheduling-warning mt-2"
|
||||
>
|
||||
<span
|
||||
class="scheduling-warning-icon svg-icon color gray-50"
|
||||
v-html="icons.alert"
|
||||
></span>
|
||||
<span
|
||||
class="scheduling-warning-text"
|
||||
v-html="schedulingWarning"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!groupId"
|
||||
class="tags-select option mt-3"
|
||||
@@ -963,6 +1012,20 @@
|
||||
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.b-popover {
|
||||
margin-top: -5px;
|
||||
max-width: 330px;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
text-align: left;
|
||||
|
||||
a {
|
||||
color: $gray-500;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
@@ -1065,6 +1128,42 @@
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.scheduling-summary {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-50;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.scheduling-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.scheduling-warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.scheduling-warning-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1196,6 +1295,7 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||
import gripIcon from '@/assets/svg/grip.svg?raw';
|
||||
import InformationIcon from '@/components/ui/informationIcon.vue';
|
||||
import alertIcon from '@/assets/svg/for-css/alert-white.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -1231,6 +1331,7 @@ export default {
|
||||
streak: streakIcon,
|
||||
calendar: calendarIcon,
|
||||
grip: gripIcon,
|
||||
alert: alertIcon,
|
||||
}),
|
||||
members: [],
|
||||
membersNameAndId: [],
|
||||
@@ -1251,6 +1352,11 @@ export default {
|
||||
{ key: 'per', label: 'perception', description: 'perTaskText' },
|
||||
],
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
spiLinkData: {
|
||||
firstLink: '<a href="/static/privacy#section_1" target="_blank" rel="noopener noreferrer">',
|
||||
secondLink: '<a href="/static/privacy" target="_blank" rel="noopener noreferrer">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -1326,6 +1432,87 @@ export default {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
schedulingSummary () {
|
||||
if (!this.task || this.task.type !== 'daily') return '';
|
||||
const { task } = this;
|
||||
const everyXValue = +task.everyX;
|
||||
|
||||
let interval;
|
||||
if (task.frequency === 'daily') {
|
||||
interval = everyXValue === 1 ? this.$t('everyDay') : this.$t('everyXDays', { count: everyXValue });
|
||||
} else if (task.frequency === 'weekly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyWeek') : this.$t('everyXWeeks', { count: everyXValue });
|
||||
} else if (task.frequency === 'monthly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyMonth') : this.$t('everyXMonths', { count: everyXValue });
|
||||
} else if (task.frequency === 'yearly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyYear') : this.$t('everyXYears', { count: everyXValue });
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
let details = '';
|
||||
if (task.frequency === 'weekly') {
|
||||
const dayNames = {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
};
|
||||
const activeDays = Object.keys(task.repeat || {}).filter(d => task.repeat[d]);
|
||||
if (activeDays.length > 0) {
|
||||
details = ` on ${activeDays.map(d => dayNames[d]).join(', ')}`;
|
||||
}
|
||||
} else if (task.frequency === 'monthly' && task.startDate) {
|
||||
const dayOfMonth = moment(task.startDate).date();
|
||||
if (task.weeksOfMonth && task.weeksOfMonth.length > 0) {
|
||||
const weekNum = task.weeksOfMonth[0] + 1;
|
||||
const weekStr = String(weekNum);
|
||||
const lastDigit = weekStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && weekStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && weekStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && weekStr !== '13') suffix = 'rd';
|
||||
const dayName = moment(task.startDate).format('dddd');
|
||||
details = ` on the ${weekNum}${suffix} ${dayName} of the month`;
|
||||
} else if (task.daysOfMonth && task.daysOfMonth.length > 0) {
|
||||
const dom = task.daysOfMonth[0];
|
||||
const domStr = String(dom);
|
||||
const lastDigit = domStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
|
||||
details = ` on the ${dom}${suffix}`;
|
||||
} else {
|
||||
const domStr = String(dayOfMonth);
|
||||
const lastDigit = domStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
|
||||
details = ` on the ${dayOfMonth}${suffix}`;
|
||||
}
|
||||
} else if (task.frequency === 'yearly' && task.startDate) {
|
||||
details = ` on ${moment(task.startDate).format('MMMM Do')}`;
|
||||
}
|
||||
|
||||
return `${this.$t('repeats')} ${interval}${details}`;
|
||||
},
|
||||
schedulingWarning () {
|
||||
if (!this.task || this.task.type !== 'daily') return '';
|
||||
const { task } = this;
|
||||
if (task.frequency === 'monthly'
|
||||
&& task.weeksOfMonth && task.weeksOfMonth.length > 0
|
||||
&& task.weeksOfMonth[0] === 4
|
||||
&& task.startDate) {
|
||||
const dayName = moment(task.startDate).format('dddd');
|
||||
return this.$t('fifthWeekWarning', { day: dayName });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
repeatsOn: {
|
||||
get () {
|
||||
let repeatsOn = 'dayOfMonth';
|
||||
@@ -1399,7 +1586,7 @@ export default {
|
||||
this.task.down = !this.task.down;
|
||||
},
|
||||
weekdaysMin (dayNumber) {
|
||||
return moment.weekdaysMin(dayNumber);
|
||||
return this.$t(`weekdaysMin${dayNumber}`);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
|
||||
@@ -222,14 +222,22 @@ export default {
|
||||
return usernames;
|
||||
},
|
||||
summarySentence () {
|
||||
let fifthWeekWarning = '';
|
||||
if (this.task.type === 'daily' && this.task.frequency === 'monthly'
|
||||
&& this.task.weeksOfMonth && this.task.weeksOfMonth.length > 0
|
||||
&& this.task.weeksOfMonth[0] === 4) {
|
||||
const activeDays = keys(pickBy(this.task.repeat, value => value === true));
|
||||
const dayName = this.expandDayString[activeDays[0]];
|
||||
fifthWeekWarning = ` ${this.$t('fifthWeekWarning', { day: dayName })}`;
|
||||
}
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.${fifthWeekWarning}`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.${fifthWeekWarning}`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
@@ -287,25 +295,14 @@ export default {
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const weekNum = weeksOfMonth[0] + 1;
|
||||
const weekNumStr = String(weekNum);
|
||||
const lastDigit = weekNumStr.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (lastDigit === '1' && weekNumStr !== '11') ordinalSuffix = 'st';
|
||||
if (lastDigit === '2' && weekNumStr !== '12') ordinalSuffix = 'nd';
|
||||
if (lastDigit === '3' && weekNumStr !== '13') ordinalSuffix = 'rd';
|
||||
dayStringArray.push(`${weekNum}${ordinalSuffix}`);
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
@@ -343,9 +340,8 @@ export default {
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
if (numericX === 1) return `<strong>${this.$t('everyMonth')}</strong>`;
|
||||
return `<strong>${this.$t('everyXMonths', { count: numericX })}</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
|
||||
@@ -68,8 +68,12 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
upDate (after) {
|
||||
this.value = after;
|
||||
this.$emit('update:date', after);
|
||||
// zero out the time so the server doesn't shift the day across a DST boundary on save
|
||||
const normalized = after
|
||||
? new Date(after.getFullYear(), after.getMonth(), after.getDate())
|
||||
: null;
|
||||
this.value = normalized;
|
||||
this.$emit('update:date', normalized);
|
||||
},
|
||||
setToday () {
|
||||
this.upDate(moment().toDate());
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
<div class="">
|
||||
<div
|
||||
class="alert alert-info alert-sm"
|
||||
v-html="$t('communityGuidelinesWarning', managerEmail)"
|
||||
v-html="$t('communityGuidelinesWarning', communityGuidelineLinks)"
|
||||
></div>
|
||||
<!-- TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak-->
|
||||
<div class="form-group">
|
||||
@@ -1060,8 +1060,10 @@ export default {
|
||||
blurb: '',
|
||||
},
|
||||
hero: {},
|
||||
managerEmail: {
|
||||
hrefBlankCommunityManagerEmail: `<a href="mailto:${COMMUNITY_MANAGER_EMAIL}">${COMMUNITY_MANAGER_EMAIL}</a>`,
|
||||
communityGuidelineLinks: {
|
||||
linkOpen: '<a href="https://habitica.com/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
|
||||
linkClose: '</a>',
|
||||
adminEmail: `<a href="mailto:${COMMUNITY_MANAGER_EMAIL}" target="_blank" rel="noopener noreferrer">${COMMUNITY_MANAGER_EMAIL}</a>`,
|
||||
},
|
||||
selectedPage: 'profile',
|
||||
achievements: {},
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
v-if="userLevel100Plus"
|
||||
v-once
|
||||
class="level-100-message"
|
||||
v-html="$t('noMoreAllocate')"
|
||||
v-html="$t('noMoreAllocate', allocateLinks)"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row allocation-boxes-row">
|
||||
@@ -395,14 +395,12 @@ export default {
|
||||
_skip: 'skip',
|
||||
shield: this.$t('offHandCapitalized'),
|
||||
},
|
||||
|
||||
allocateStatsList: {
|
||||
str: { title: 'allocateStr', popover: 'strengthText', allocatepop: 'allocateStrPop' },
|
||||
int: { title: 'allocateInt', popover: 'intText', allocatepop: 'allocateIntPop' },
|
||||
con: { title: 'allocateCon', popover: 'conText', allocatepop: 'allocateConPop' },
|
||||
per: { title: 'allocatePer', popover: 'perText', allocatepop: 'allocatePerPop' },
|
||||
},
|
||||
|
||||
stats: {
|
||||
str: {
|
||||
title: 'strength',
|
||||
@@ -422,6 +420,10 @@ export default {
|
||||
},
|
||||
},
|
||||
content: Content,
|
||||
allocateLinks: {
|
||||
linkOpen: '<a href="/shops/market">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
v-if="userLevel100Plus"
|
||||
v-once
|
||||
>
|
||||
{{ $t('noMoreAllocate') }}
|
||||
{{ $t('noMoreAllocate', allocateLinks) }}
|
||||
</p>
|
||||
<p
|
||||
v-if="user.stats.points || userLevel100Plus"
|
||||
@@ -480,6 +480,10 @@ export default {
|
||||
con: { title: 'allocateCon', popover: 'conText', allocatepop: 'allocateConPop' },
|
||||
per: { title: 'allocatePer', popover: 'perText', allocatepop: 'allocatePerPop' },
|
||||
},
|
||||
allocateLinks: {
|
||||
linkOpen: '<a href="/shops/market">',
|
||||
linkClose: '</a>',
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user