Compare commits
97 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea398f6294 | ||
|
|
5f41042826 | ||
|
|
486b7d4da1 | ||
|
|
91b47e56ff | ||
|
|
9934e59629 | ||
|
|
50cc66d51c | ||
|
|
936c9dc4f3 | ||
|
|
946ade5da1 | ||
|
|
80068a3674 | ||
|
|
d7c9a7874b | ||
|
|
768e5b3f5b | ||
|
|
f3320d9ae3 | ||
|
|
d4538b0909 | ||
|
|
676ee74f19 | ||
|
|
9059f227fa | ||
|
|
6a14d0f3f3 | ||
|
|
3e5c623125 | ||
|
|
e559fb7e4b | ||
|
|
88a1cfb689 | ||
|
|
f12c4e75e6 | ||
|
|
90f08c58cd | ||
|
|
f6aa96c64c | ||
|
|
2b04a1b50c | ||
|
|
7297fb5241 | ||
|
|
98c5a68a8c | ||
|
|
8e643747f8 | ||
|
|
2483e19bee | ||
|
|
f9d3c6ed48 | ||
|
|
09a0e75351 | ||
|
|
644edc5b76 | ||
|
|
a64b994376 | ||
|
|
fb626ebf7e | ||
|
|
dd334f487e | ||
|
|
cd5c86fb69 | ||
|
|
7878761b6f | ||
|
|
d3b63abdd3 | ||
|
|
23fad37205 | ||
|
|
88558e6b98 | ||
|
|
a84ee8497b | ||
|
|
d560ee2da1 | ||
|
|
fd3fce110e | ||
|
|
1bce2b0e28 | ||
|
|
06a59bfe03 | ||
|
|
83a430afad | ||
|
|
949f638b6e | ||
|
|
2b2193e9ce | ||
|
|
0709bada87 | ||
|
|
506586b74c | ||
|
|
99b2ee273f | ||
|
|
aa6e536851 | ||
|
|
2a2c1af7ba | ||
|
|
48e381d702 | ||
|
|
9aafd76746 | ||
|
|
0069af78a3 | ||
|
|
c25fe7eb3d | ||
|
|
b9a9013685 | ||
|
|
54d075e4fd | ||
|
|
1c40044525 | ||
|
|
5784694dc9 | ||
|
|
7af4a6ff11 | ||
|
|
a601be0666 | ||
|
|
1be169a105 | ||
|
|
6b02af69f2 | ||
|
|
1fe4bd2de7 | ||
|
|
afd00a8ab6 | ||
|
|
63918b3c20 | ||
|
|
6293a4b936 | ||
|
|
44502092ad | ||
|
|
ce0e8284fe | ||
|
|
15f104ddd0 | ||
|
|
7f6ae8ffbf | ||
|
|
b2ecfb5a32 | ||
|
|
fa6ba8b668 | ||
|
|
826dffc794 | ||
|
|
688190ac4a | ||
|
|
4909a3b537 | ||
|
|
64e2150f44 | ||
|
|
3f7abc459c | ||
|
|
618cdafd10 | ||
|
|
e7b37d0378 | ||
|
|
3f3e2525d2 | ||
|
|
765e08f999 | ||
|
|
598bc29647 | ||
|
|
39ccddfb1c | ||
|
|
108214a217 | ||
|
|
271f40e355 | ||
|
|
e801547580 | ||
|
|
fc11941186 | ||
|
|
882fad3113 | ||
|
|
6168492711 | ||
|
|
2aade9aaa6 | ||
|
|
4789946c4e | ||
|
|
a69d8877c9 | ||
|
|
7cc0c3bc57 | ||
|
|
fb78495a1b | ||
|
|
22def5111f | ||
|
|
6e91d51def |
30
.github/workflows/test.yml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -41,7 +42,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -63,7 +65,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -86,7 +89,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -108,7 +112,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -137,7 +142,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -167,7 +173,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -197,7 +204,8 @@ jobs:
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -222,7 +230,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
@@ -246,7 +255,8 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: sudo apt-get -y install libkrb5-dev
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
- name: npm install
|
||||
run: |
|
||||
|
||||
25
.heroku/report_deploy.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
DEVELOPER="someone"
|
||||
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
DEVELOPERS=$(git log -5 --pretty=format:'%an')
|
||||
IFS=$'\n'
|
||||
DEVELOPER=""
|
||||
for dev in $DEVELOPERS
|
||||
do
|
||||
if [ "$DEVELOPER" == "someone" ]; then
|
||||
if [[ ${dev} != *"[bot]"* ]]; then
|
||||
DEVELOPER=$dev
|
||||
continue
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
PARTS=$(cut -d"." -f1 <<< $BASE_URL)
|
||||
SERVER_NAME=$(cut -d"/" -f3 <<< ${PARTS[0]})
|
||||
|
||||
SERVER_NAME=":$SERVER_EMOJI: $SERVER_NAME"
|
||||
|
||||
wget $SLACK_DEPLOY_URL --post-data="{\"server_name\": \"$SERVER_NAME\", \"developer\": \"$DEVELOPER\", \"base_url\": \"$BASE_URL\"}" -O /dev/null
|
||||
@@ -37,6 +37,7 @@
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
|
||||
@@ -42,10 +42,41 @@ function cssVarMap (sprite) {
|
||||
}
|
||||
}
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
function filterFile (file) {
|
||||
if (file.relative.indexOf('Mount_Icon_') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('shop/') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/eggs') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/food') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.path.indexOf('stable/potions') !== -1) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('shop_') === 0) {
|
||||
return false;
|
||||
}
|
||||
if (file.relative.indexOf('icon_background') === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function createSpritesStream (name, src) {
|
||||
const stream = mergeStream();
|
||||
// need to import this way bc of weird dependency things
|
||||
// eslint-disable-next-line global-require
|
||||
const filter = require('gulp-filter');
|
||||
|
||||
const f = filter(filterFile);
|
||||
|
||||
const spriteData = gulp.src(src)
|
||||
.pipe(f)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}.png`,
|
||||
cssName: `spritesmith-${name}.css`,
|
||||
@@ -63,7 +94,7 @@ function createSpritesStream (name, src) {
|
||||
return stream;
|
||||
}
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
gulp.task('sprites:main', async () => {
|
||||
const mainSrc = sync('habitica-images/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
|
||||
47
migrations/archive/2024/2024_purge_invite_accepted.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '2024_purge_invite_accepted';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUsers (userIds) {
|
||||
count += userIds.length;
|
||||
if (count % progressCount === 0) console.warn(`${count} ${userIds[0]}`);
|
||||
|
||||
return await User.updateMany(
|
||||
{ _id: { $in: userIds } },
|
||||
{ $pull: { notifications: { type: 'GROUP_INVITE_ACCEPTED' } } },
|
||||
).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'notifications.type': 'GROUP_INVITE_ACCEPTED',
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-06-25') },
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({ _id: 1 })
|
||||
.select({ _id: 1 })
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
const userIds = users.map(user => user._id);
|
||||
|
||||
await updateUsers(userIds); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,14 +1,12 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20230731_naming_day';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20240731_naming_day';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
count += 1;
|
||||
|
||||
let set;
|
||||
let push;
|
||||
@@ -115,16 +113,16 @@ async function updateUser (user) {
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (push) {
|
||||
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
|
||||
} else {
|
||||
return await user.updateOne({ $set: set, $inc: inc }).exec();
|
||||
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
|
||||
}
|
||||
|
||||
return user.updateOne({ $set: set, $inc: inc }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2024-07-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
@@ -136,7 +134,7 @@ export default async function processUsers () {
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.sort({ _id: 1 })
|
||||
.select(fields)
|
||||
.exec();
|
||||
|
||||
@@ -152,4 +150,4 @@ export default async function processUsers () {
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
}
|
||||
925
package-lock.json
generated
10
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.26.2",
|
||||
"version": "5.28.6",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -15,6 +15,7 @@
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.54.0",
|
||||
"apple-auth": "^1.0.9",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"bootstrap": "^4.6.2",
|
||||
@@ -35,6 +36,7 @@
|
||||
"got": "^11.8.6",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
@@ -74,6 +76,7 @@
|
||||
"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"
|
||||
@@ -104,14 +107,15 @@
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "gulp nodemon",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "gulp nodemon --inspect",
|
||||
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": "npm run client:build"
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"axios": "^1.7.4",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
|
||||
@@ -54,6 +54,7 @@ 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;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -71,6 +72,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
sandbox.stub(logger, 'error');
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
@@ -104,6 +106,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
|
||||
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;
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
@@ -120,6 +123,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
|
||||
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;
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -135,6 +139,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
|
||||
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;
|
||||
|
||||
req.query.liveliness = '';
|
||||
@@ -150,6 +155,7 @@ 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;
|
||||
|
||||
// call for 31 times
|
||||
@@ -173,6 +179,7 @@ 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;
|
||||
|
||||
req.ip = 1;
|
||||
@@ -199,4 +206,51 @@ describe('rateLimiter middleware', () => {
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
req.headers['x-api-user'] = 'user-1';
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
// user id an ip are counted as separate sources
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 27, // 2 calls with user id
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
|
||||
req.headers['x-api-user'] = undefined;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 24, // 3 calls with only ip
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(res.set).to.have.been.calledWithMatch({
|
||||
'X-RateLimit-Limit': 30,
|
||||
'X-RateLimit-Remaining': 10,
|
||||
'X-RateLimit-Reset': sinon.match(Date),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
73
test/api/v3/integration/debug/POST-debug_boss-rage.test.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /debug/boss-rage', () => {
|
||||
let user;
|
||||
let nconfStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nconfStub = sandbox.stub(nconf, 'get');
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
|
||||
nconfStub.withArgs('BASE_URL').returns('https://example.com');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nconfStub.restore();
|
||||
});
|
||||
|
||||
it('errors if user is not in a party', async () => {
|
||||
await expect(user.post('/debug/boss-rage'))
|
||||
.to.eventually.be.rejected.and.deep.equal({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User not in a party.',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when not in production mode', async () => {
|
||||
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
|
||||
|
||||
await expect(user.post('/debug/boss-rage'))
|
||||
.to.eventually.be.rejected.and.deep.equal({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: 'Not found.',
|
||||
});
|
||||
});
|
||||
|
||||
context('user is in a party', async () => {
|
||||
let party;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Test Party',
|
||||
type: 'party',
|
||||
},
|
||||
members: 2,
|
||||
});
|
||||
party = group;
|
||||
user = groupLeader;
|
||||
});
|
||||
|
||||
it('increases boss rage to 50', async () => {
|
||||
await user.post('/debug/boss-rage');
|
||||
await party.sync();
|
||||
expect(party.quest.progress.rage).to.eql(50);
|
||||
});
|
||||
|
||||
it('increases boss rage to 100', async () => {
|
||||
await user.post('/debug/boss-rage');
|
||||
await user.post('/debug/boss-rage');
|
||||
await party.sync();
|
||||
expect(party.quest.progress.rage).to.eql(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -34,9 +34,11 @@ describe('POST /debug/jump-time', () => {
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
|
||||
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
|
||||
expect(newResultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const tomorrow = new Date(today.valueOf());
|
||||
tomorrow.setDate(today.getDate() + 1);
|
||||
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
|
||||
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
|
||||
});
|
||||
|
||||
it('jumps back', async () => {
|
||||
@@ -45,9 +47,11 @@ describe('POST /debug/jump-time', () => {
|
||||
expect(resultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
|
||||
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
|
||||
expect(newResultDate.getMonth()).to.eql(today.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
|
||||
const yesterday = new Date(today.valueOf());
|
||||
yesterday.setDate(today.getDate() - 1);
|
||||
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
|
||||
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
|
||||
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
|
||||
});
|
||||
|
||||
it('can jump a lot', async () => {
|
||||
|
||||
@@ -85,22 +85,6 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 1);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
const inviter = await user.get('/user');
|
||||
const expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: guild.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[1].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('awards Joined Guild achievement', async () => {
|
||||
await invitedUser.post(`/groups/${guild._id}/join`);
|
||||
|
||||
@@ -155,23 +139,6 @@ describe('POST /group/:groupId/join', () => {
|
||||
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
|
||||
});
|
||||
|
||||
it('notifies inviting user that their invitation was accepted', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
const inviter = await user.get('/user');
|
||||
|
||||
const expectedData = {
|
||||
headerText: t('invitationAcceptedHeader'),
|
||||
bodyText: t('invitationAcceptedBody', {
|
||||
username: invitedUser.auth.local.username,
|
||||
groupName: party.name,
|
||||
}),
|
||||
};
|
||||
|
||||
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
|
||||
expect(inviter.notifications[0].data).to.eql(expectedData);
|
||||
});
|
||||
|
||||
it('clears invitation from user when joining party', async () => {
|
||||
await invitedUser.post(`/groups/${party._id}/join`);
|
||||
|
||||
|
||||
@@ -125,6 +125,90 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(body.finalLvl).to.eql(user.stats.lvl);
|
||||
});
|
||||
});
|
||||
|
||||
context('handles drops', async () => {
|
||||
let randomStub;
|
||||
|
||||
afterEach(() => {
|
||||
randomStub.restore();
|
||||
});
|
||||
it('gives user a drop', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not give a drop when non-sub drop cap is reached', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 5,
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.undefined;
|
||||
});
|
||||
|
||||
it('gives a drop when subscriber is over regular cap but under subscriber cap', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 6,
|
||||
'purchased.plan.customerId': '123',
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.ok;
|
||||
});
|
||||
|
||||
it('does not give a drop when subscriber is at subscriber drop cap', async () => {
|
||||
user = await generateUser({
|
||||
'stats.gp': 100,
|
||||
'achievements.completedTask': true,
|
||||
'items.eggs': {
|
||||
Wolf: 1,
|
||||
},
|
||||
'items.lastDrop.count': 10,
|
||||
'purchased.plan.customerId': '123',
|
||||
});
|
||||
randomStub = sandbox.stub(Math, 'random').returns(0.1);
|
||||
const task = await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
const res = await user.post(`/tasks/${task.id}/score/up`);
|
||||
expect(res._tmp.drop).to.be.undefined;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('todos', () => {
|
||||
|
||||
@@ -105,9 +105,9 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(member.notifications[1].taskId).to.equal(groupTask._id);
|
||||
const lastNotification = member.notifications[member.notifications.length - 1];
|
||||
expect(lastNotification.type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(lastNotification.taskId).to.equal(groupTask._id);
|
||||
});
|
||||
|
||||
it('assigns a task to multiple users', async () => {
|
||||
|
||||
@@ -89,10 +89,12 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
});
|
||||
|
||||
it('removes task assignment notification from unassigned user', async () => {
|
||||
await member.sync();
|
||||
const oldNotificationCount = member.notifications.length;
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
expect(member.notifications.length).to.equal(1); // mystery items
|
||||
expect(member.notifications.length).to.equal(oldNotificationCount - 1);
|
||||
});
|
||||
|
||||
it('unassigns a user and only that user from a task', async () => {
|
||||
|
||||
@@ -123,7 +123,7 @@ describe('GET /world-state', () => {
|
||||
|
||||
const res = await requester().get('/world-state');
|
||||
|
||||
expect(res.npcImageSuffix).to.equal('winter');
|
||||
expect(res.npcImageSuffix).to.equal('fall');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,24 @@ describe('GET /user', () => {
|
||||
expect(returnedUser.stats).to.not.exist;
|
||||
});
|
||||
|
||||
it('returns when ALWAYS_LOADED paths are requested', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=_id,notifications,preferences,auth,flags,permissions');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.notifications).to.exist;
|
||||
expect(returnedUser.preferences).to.exist;
|
||||
expect(returnedUser.auth).to.exist;
|
||||
expect(returnedUser.flags).to.exist;
|
||||
expect(returnedUser.permissions).to.exist;
|
||||
});
|
||||
|
||||
it('returns when subpaths paths are requested', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=auth.local.username');
|
||||
|
||||
expect(returnedUser._id).to.equal(user._id);
|
||||
expect(returnedUser.auth.local.username).to.exist;
|
||||
});
|
||||
|
||||
it('does not return requested private properties', async () => {
|
||||
const returnedUser = await user.get('/user?userFields=apiToken,secret.text');
|
||||
|
||||
|
||||
@@ -47,15 +47,17 @@ describe('shops', () => {
|
||||
|
||||
describe('premium hatching potions', () => {
|
||||
it('contains current scheduled premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.length).to.eql(2);
|
||||
expect(potions.items.length).to.eql(3);
|
||||
});
|
||||
|
||||
it('does not contain past scheduled premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
|
||||
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length, 'Aquatic or Celestial found').to.eql(0);
|
||||
});
|
||||
|
||||
it('returns end date for scheduled premium potions', async () => {
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
potions.items.forEach(potion => {
|
||||
@@ -73,9 +75,9 @@ describe('shops', () => {
|
||||
});
|
||||
|
||||
it('does not contain locked quest premium hatching potions', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
|
||||
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
|
||||
expect(potions.items.length).to.eql(2);
|
||||
expect(potions.items.length).to.eql(3);
|
||||
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
|
||||
});
|
||||
|
||||
@@ -341,6 +343,16 @@ describe('shops', () => {
|
||||
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
|
||||
expect(backgrounds.length).to.be.greaterThan(0);
|
||||
});
|
||||
|
||||
it('does not add an end date to steampunk gear', () => {
|
||||
const categories = shopCategories.filter(cat => cat.identifier.startsWith('30'));
|
||||
categories.forEach(category => {
|
||||
expect(category.end).to.not.exist;
|
||||
category.items.forEach(item => {
|
||||
expect(item.end).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('customizationShop', () => {
|
||||
|
||||
@@ -233,6 +233,17 @@ describe('shared.ops.purchase', () => {
|
||||
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases event hatching potion', async () => {
|
||||
clock.restore();
|
||||
clock = sandbox.useFakeTimers(moment('2022-04-10').valueOf());
|
||||
const type = 'hatchingPotions';
|
||||
const key = 'Veggie';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
expect(user.items.hatchingPotions[key]).to.eql(1);
|
||||
});
|
||||
|
||||
it('purchases hatching potion if user completed quest', async () => {
|
||||
const type = 'hatchingPotions';
|
||||
const key = 'Bronze';
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('events', () => {
|
||||
});
|
||||
|
||||
it('returns empty array when no events are active', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-06'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.be.empty;
|
||||
});
|
||||
@@ -27,14 +27,14 @@ describe('events', () => {
|
||||
it('returns nye event at beginning of the year', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2025-01-01'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.have.length(1);
|
||||
expect(events).to.have.length(2);
|
||||
expect(events[0].key).to.equal('nye');
|
||||
});
|
||||
|
||||
it('returns nye event at end of the year', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-12-30'));
|
||||
const events = getRepeatingEvents();
|
||||
expect(events).to.have.length(1);
|
||||
expect(events).to.have.length(2);
|
||||
expect(events[0].key).to.equal('nye');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe('food', () => {
|
||||
});
|
||||
|
||||
it('sets canDrop for pie if it is pie season', () => {
|
||||
clock = sinon.useFakeTimers(new Date(2024, 2, 14));
|
||||
clock = sinon.useFakeTimers(new Date(2024, 2, 15));
|
||||
const datedContent = require('../../website/common/script/content').default;
|
||||
each(datedContent.food, foodItem => {
|
||||
if (foodItem.key.indexOf('Pie_') !== -1) {
|
||||
|
||||
@@ -42,23 +42,23 @@ describe('content index', () => {
|
||||
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
|
||||
});
|
||||
|
||||
it('Releases pets gear when appropriate without needing restarting', () => {
|
||||
it('Releases pets when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const junePets = content.petInfo;
|
||||
expect(junePets['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||
const julyPets = content.petInfo;
|
||||
expect(julyPets['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
|
||||
});
|
||||
|
||||
it('Releases mounts gear when appropriate without needing restarting', () => {
|
||||
it('Releases mounts when appropriate without needing restarting', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-06-20'));
|
||||
const juneMounts = content.mountInfo;
|
||||
expect(juneMounts['Chameleon-Base']).to.not.exist;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-10'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-07-18'));
|
||||
const julyMounts = content.mountInfo;
|
||||
expect(julyMounts['Chameleon-Base']).to.exist;
|
||||
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
|
||||
@@ -131,7 +131,7 @@ describe('content index', () => {
|
||||
});
|
||||
|
||||
it('marks pie as buyable and droppable during pi day', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-03-14'));
|
||||
clock = sinon.useFakeTimers(new Date('2024-03-15'));
|
||||
const { food } = content;
|
||||
Object.keys(food).forEach(key => {
|
||||
if (key === 'Saddle') {
|
||||
|
||||
@@ -18,12 +18,19 @@ function validateMatcher (matcher, checkedDate) {
|
||||
|
||||
describe('Content Schedule', () => {
|
||||
let switchoverTime;
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
|
||||
clearCachedMatchers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('assembles scheduled items on january 15th', () => {
|
||||
const date = new Date('2024-01-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
@@ -105,8 +112,14 @@ describe('Content Schedule', () => {
|
||||
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date if its on the release day', () => {
|
||||
const date = new Date('2024-05-07T07:00:00.000Z');
|
||||
it('sets the end date if its on the release day before switchover', () => {
|
||||
const date = new Date('2024-05-07T07:00:00.000+00:00');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date if its on the release day after switchover', () => {
|
||||
const date = new Date('2024-05-07T09:00:00.000+00:00');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
@@ -123,12 +136,54 @@ describe('Content Schedule', () => {
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('sets the end date for a winter gala', () => {
|
||||
const date = new Date('2024-12-22');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
|
||||
});
|
||||
|
||||
it('uses correct date for first hours of the month', () => {
|
||||
// if the date is checked before CONTENT_SWITCHOVER_TIME_OFFSET,
|
||||
// it should be considered the previous month
|
||||
const date = new Date('2024-05-01T02:00:00.000Z');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.petQuests.items).to.contain('snake');
|
||||
expect(matchers.petQuests.items).to.not.contain('horse');
|
||||
expect(matchers.timeTravelers.match('202304'), '202304').to.be.true;
|
||||
expect(matchers.timeTravelers.match('202404'), '202404').to.be.false;
|
||||
expect(matchers.timeTravelers.match('202305'), '202305').to.be.false;
|
||||
});
|
||||
|
||||
it('uses correct date after switchover time', () => {
|
||||
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
|
||||
// it should be considered the current
|
||||
const date = new Date('2024-05-01T09:00:00.000Z');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.petQuests.items).to.contain('snake');
|
||||
expect(matchers.petQuests.items).to.not.contain('horse');
|
||||
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
|
||||
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
|
||||
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
|
||||
});
|
||||
|
||||
it('uses UTC timezone', () => {
|
||||
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
|
||||
// it should be considered the current
|
||||
clock = sinon.useFakeTimers(new Date('2024-05-01T05:00:00.000-04:00'));
|
||||
const matchers = getAllScheduleMatchingGroups();
|
||||
expect(matchers.petQuests.items).to.contain('snake');
|
||||
expect(matchers.petQuests.items).to.not.contain('horse');
|
||||
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
|
||||
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
|
||||
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
|
||||
});
|
||||
|
||||
it('contains content for repeating events', () => {
|
||||
const date = new Date('2024-04-15');
|
||||
const matchers = getAllScheduleMatchingGroups(date);
|
||||
expect(matchers.premiumHatchingPotions).to.exist;
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
|
||||
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
|
||||
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
|
||||
});
|
||||
|
||||
@@ -245,27 +300,33 @@ describe('Content Schedule', () => {
|
||||
it('allows sets matching the month', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||
expect(matcher.match('202307')).to.be.true;
|
||||
expect(matcher.match('202207')).to.be.true;
|
||||
expect(matcher.match('202307'), '202307').to.be.true;
|
||||
expect(matcher.match('202207'), '202207').to.be.true;
|
||||
});
|
||||
|
||||
it('disallows sets not matching the month', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||
expect(matcher.match('202306')).to.be.false;
|
||||
expect(matcher.match('202402')).to.be.false;
|
||||
expect(matcher.match('202306'), '202306').to.be.false;
|
||||
expect(matcher.match('202402'), '202402').to.be.false;
|
||||
});
|
||||
|
||||
it('disallows sets from current month', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||
expect(matcher.match('202407')).to.be.false;
|
||||
expect(matcher.match('202407'), '202407').to.be.false;
|
||||
});
|
||||
|
||||
it('disallows sets from the future', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
|
||||
expect(matcher.match('202507')).to.be.false;
|
||||
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||
expect(matcher.match('202507'), '202507').to.be.false;
|
||||
});
|
||||
|
||||
it('matches sets released in the earlier half of the year', () => {
|
||||
const date = new Date('2024-07-08');
|
||||
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
|
||||
expect(matcher.match('202401'), '202401').to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('time-travelers store', () => {
|
||||
|
||||
describe('on may 1st', () => {
|
||||
beforeEach(() => {
|
||||
date = new Date('2024-05-01');
|
||||
date = new Date('2024-05-01T09:00:00.000Z');
|
||||
});
|
||||
it('returns the correct gear', () => {
|
||||
const items = timeTravelers.timeTravelerStore(user, date);
|
||||
|
||||
168
website/client/package-lock.json
generated
@@ -23,7 +23,6 @@
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"core-js": "^3.33.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
@@ -59,7 +58,7 @@
|
||||
"chai": "^5.1.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"webpack": "^5.89.0"
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -2307,15 +2306,6 @@
|
||||
"@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==",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
|
||||
@@ -3129,9 +3119,9 @@
|
||||
"integrity": "sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
|
||||
"integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
||||
"integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6"
|
||||
@@ -3148,9 +3138,9 @@
|
||||
"integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
|
||||
"integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz",
|
||||
"integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.11.6",
|
||||
@@ -3168,14 +3158,14 @@
|
||||
"integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
|
||||
"integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz",
|
||||
"integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@webassemblyjs/helper-buffer": "1.12.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6"
|
||||
"@webassemblyjs/wasm-gen": "1.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
@@ -3200,26 +3190,26 @@
|
||||
"integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
|
||||
"integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz",
|
||||
"integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@webassemblyjs/helper-buffer": "1.12.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-section": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6",
|
||||
"@webassemblyjs/wasm-opt": "1.11.6",
|
||||
"@webassemblyjs/wasm-parser": "1.11.6",
|
||||
"@webassemblyjs/wast-printer": "1.11.6"
|
||||
"@webassemblyjs/helper-wasm-section": "1.12.1",
|
||||
"@webassemblyjs/wasm-gen": "1.12.1",
|
||||
"@webassemblyjs/wasm-opt": "1.12.1",
|
||||
"@webassemblyjs/wasm-parser": "1.12.1",
|
||||
"@webassemblyjs/wast-printer": "1.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
|
||||
"integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz",
|
||||
"integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/ieee754": "1.11.6",
|
||||
"@webassemblyjs/leb128": "1.11.6",
|
||||
@@ -3227,22 +3217,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
|
||||
"integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz",
|
||||
"integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/helper-buffer": "1.11.6",
|
||||
"@webassemblyjs/wasm-gen": "1.11.6",
|
||||
"@webassemblyjs/wasm-parser": "1.11.6"
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@webassemblyjs/helper-buffer": "1.12.1",
|
||||
"@webassemblyjs/wasm-gen": "1.12.1",
|
||||
"@webassemblyjs/wasm-parser": "1.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
|
||||
"integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz",
|
||||
"integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@webassemblyjs/helper-api-error": "1.11.6",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.11.6",
|
||||
"@webassemblyjs/ieee754": "1.11.6",
|
||||
@@ -3251,11 +3241,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
|
||||
"integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz",
|
||||
"integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.11.6",
|
||||
"@webassemblyjs/ast": "1.12.1",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
@@ -3326,10 +3316,10 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-import-assertions": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
|
||||
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
|
||||
"node_modules/acorn-import-attributes": {
|
||||
"version": "1.9.5",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
|
||||
"integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8"
|
||||
}
|
||||
@@ -4046,11 +4036,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -5398,11 +5388,6 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.6.tgz",
|
||||
"integrity": "sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w=="
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
@@ -5496,9 +5481,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
|
||||
"integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
@@ -6871,9 +6856,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -9008,11 +8993,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -13342,9 +13327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
@@ -13378,33 +13363,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.89.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz",
|
||||
"integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==",
|
||||
"version": "5.94.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
|
||||
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
"@types/estree": "^1.0.0",
|
||||
"@webassemblyjs/ast": "^1.11.5",
|
||||
"@webassemblyjs/wasm-edit": "^1.11.5",
|
||||
"@webassemblyjs/wasm-parser": "^1.11.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@webassemblyjs/ast": "^1.12.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.12.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.12.1",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.9.0",
|
||||
"browserslist": "^4.14.5",
|
||||
"acorn-import-attributes": "^1.9.5",
|
||||
"browserslist": "^4.21.10",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.15.0",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"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.9",
|
||||
"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": "^3.2.0",
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.7",
|
||||
"watchpack": "^2.4.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"core-js": "^3.33.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
@@ -61,6 +60,6 @@
|
||||
"chai": "^5.1.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"webpack": "^5.89.0"
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
BIN
website/client/public/static/npc/birthday/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
website/client/public/static/npc/fall/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
website/client/public/static/npc/nye/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
website/client/public/static/npc/spring/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
BIN
website/client/public/static/npc/summer/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
website/client/public/static/npc/winter/customizations_npc.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
@@ -27,73 +27,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="app"
|
||||
:class="{
|
||||
'casting-spell': castingSpell,
|
||||
}"
|
||||
>
|
||||
<!-- <banned-account-modal /> -->
|
||||
<amazon-payments-modal v-if="!isStaticPage" />
|
||||
<payments-success-modal />
|
||||
<sub-cancel-modal-confirm v-if="isUserLoaded" />
|
||||
<sub-canceled-modal v-if="isUserLoaded" />
|
||||
<bug-report-modal v-if="isUserLoaded" />
|
||||
<bug-report-success-modal v-if="isUserLoaded" />
|
||||
<external-link-modal />
|
||||
<birthday-modal />
|
||||
<snackbars />
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<template v-else>
|
||||
<template v-if="isUserLoaded">
|
||||
<chat-banner />
|
||||
<damage-paused-banner />
|
||||
<gems-promo-banner />
|
||||
<gift-promo-banner />
|
||||
<birthday-banner />
|
||||
<notifications-display />
|
||||
<app-menu />
|
||||
<div
|
||||
class="container-fluid"
|
||||
:class="{'no-margin': noMargin}"
|
||||
>
|
||||
<app-header />
|
||||
<buyModal
|
||||
:item="selectedItemToBuy || {}"
|
||||
:with-pin="true"
|
||||
:generic-purchase="genericPurchase(selectedItemToBuy)"
|
||||
@buyPressed="customPurchase($event)"
|
||||
/>
|
||||
<selectMembersModal
|
||||
:item="selectedSpellToBuy || {}"
|
||||
:group="user.party"
|
||||
@memberSelected="memberSelected($event)"
|
||||
/>
|
||||
<div :class="{sticky: user.preferences.stickyHeader}">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<app-footer v-if="!hideFooter" />
|
||||
<audio
|
||||
id="sound"
|
||||
ref="sound"
|
||||
autoplay="autoplay"
|
||||
></audio>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<snackbars />
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<user-main v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#loading-screen-inapp {
|
||||
#melior {
|
||||
color: $white;
|
||||
@@ -163,68 +105,20 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import birthdayModal from '@/components/news/birthdayModal';
|
||||
import AppMenu from './components/header/menu';
|
||||
import AppHeader from './components/header/index';
|
||||
import ChatBanner from './components/header/banners/chatBanner';
|
||||
import DamagePausedBanner from './components/header/banners/damagePaused';
|
||||
import GemsPromoBanner from './components/header/banners/gemsPromo';
|
||||
import GiftPromoBanner from './components/header/banners/giftPromo';
|
||||
import BirthdayBanner from './components/header/banners/birthdayBanner';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import BuyModal from './components/shops/buyModal.vue';
|
||||
import SelectMembersModal from '@/components/selectMembersModal.vue';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import { setup as setupPayments } from '@/libs/payments';
|
||||
import amazonPaymentsModal from '@/components/payments/amazonModal';
|
||||
import paymentsSuccessModal from '@/components/payments/successModal';
|
||||
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
|
||||
import subCanceledModal from '@/components/payments/canceledModal';
|
||||
import externalLinkModal from '@/components/externalLinkModal.vue';
|
||||
|
||||
import spellsMixin from '@/mixins/spells';
|
||||
import {
|
||||
CONSTANTS,
|
||||
getLocalSetting,
|
||||
removeLocalSetting,
|
||||
} from '@/libs/userlocalManager';
|
||||
|
||||
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
|
||||
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
|
||||
import { mapState } from '@/libs/store';
|
||||
import userMain from '@/pages/user-main';
|
||||
import snackbars from '@/components/snackbars/notifications';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AppMenu,
|
||||
AppHeader,
|
||||
AppFooter,
|
||||
birthdayModal,
|
||||
ChatBanner,
|
||||
DamagePausedBanner,
|
||||
GemsPromoBanner,
|
||||
GiftPromoBanner,
|
||||
BirthdayBanner,
|
||||
notificationsDisplay,
|
||||
snackbars,
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
amazonPaymentsModal,
|
||||
paymentsSuccessModal,
|
||||
subCancelModalConfirm,
|
||||
subCanceledModal,
|
||||
bugReportModal,
|
||||
bugReportSuccessModal,
|
||||
externalLinkModal,
|
||||
userMain,
|
||||
},
|
||||
mixins: [notifications, spellsMixin],
|
||||
data () {
|
||||
return {
|
||||
selectedItemToBuy: null,
|
||||
@@ -238,71 +132,25 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState(['isUserLoggedIn', 'isUserLoaded', 'notificationsRemoved']),
|
||||
...mapState({ user: 'user.data' }),
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
},
|
||||
castingSpell () {
|
||||
return this.$store.state.spellOptions.castingSpell;
|
||||
},
|
||||
noMargin () {
|
||||
return ['privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
hideFooter () {
|
||||
return ['privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('playSound', sound => {
|
||||
const theme = this.user.preferences.sound;
|
||||
|
||||
if (!theme || theme === 'off') {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = `/static/audio/${theme}/${sound}`;
|
||||
|
||||
if (this.audioSuffix === null) {
|
||||
this.audioSource = document.createElement('source');
|
||||
if (this.$refs.sound.canPlayType('audio/ogg')) {
|
||||
this.audioSuffix = '.ogg';
|
||||
this.audioSource.type = 'audio/ogg';
|
||||
} else {
|
||||
this.audioSuffix = '.mp3';
|
||||
this.audioSource.type = 'audio/mp3';
|
||||
}
|
||||
this.audioSource.src = file + this.audioSuffix;
|
||||
this.$refs.sound.appendChild(this.audioSource);
|
||||
} else {
|
||||
this.audioSource.src = file + this.audioSuffix;
|
||||
}
|
||||
|
||||
this.$refs.sound.load();
|
||||
// Setup listener for title
|
||||
this.$store.watch(state => state.title, title => {
|
||||
document.title = title;
|
||||
});
|
||||
|
||||
// @TODO: I'm not sure these should be at the app level.
|
||||
// Can we move these back into shop/inventory or maybe they need a lateral move?
|
||||
this.$root.$on('buyModal::showItem', item => {
|
||||
this.selectedItemToBuy = item;
|
||||
this.$root.$emit('bv::show::modal', 'buy-modal');
|
||||
});
|
||||
|
||||
this.$root.$on('bv::modal::hidden', event => {
|
||||
if (event.componentId === 'buy-modal') {
|
||||
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
|
||||
this.$store.watch(state => state.isUserLoaded, () => {
|
||||
if (this.isUserLoaded) {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
});
|
||||
|
||||
this.$root.$on('selectMembersModal::showItem', item => {
|
||||
this.selectedSpellToBuy = item;
|
||||
this.$root.$emit('bv::show::modal', 'select-member-modal');
|
||||
});
|
||||
|
||||
// @TODO split up this file, it's too big
|
||||
|
||||
loadProgressBar({
|
||||
showSpinner: false,
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
Analytics.load();
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
@@ -414,79 +262,20 @@ export default {
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
// Setup listener for title
|
||||
this.$store.watch(state => state.title, title => {
|
||||
document.title = title;
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
Analytics.load();
|
||||
});
|
||||
|
||||
if (this.isUserLoggedIn && !this.isStaticPage) {
|
||||
// Load the user and the user tasks
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch'),
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
Analytics.setUser();
|
||||
Analytics.updateUser();
|
||||
return axios.get(
|
||||
'/api/v4/i18n/browser-script',
|
||||
{
|
||||
language: this.user.preferences.language,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
},
|
||||
},
|
||||
);
|
||||
}).then(() => {
|
||||
const i18nData = window && window['habitica-i18n'];
|
||||
this.$loadLocale(i18nData);
|
||||
this.hideLoadingScreen();
|
||||
|
||||
// Adjust the timezone offset
|
||||
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
|
||||
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
|
||||
this.$store.dispatch('user:set', {
|
||||
'preferences.timezoneOffset': browserTimezoneOffset,
|
||||
});
|
||||
}
|
||||
|
||||
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
if (appState) {
|
||||
appState = JSON.parse(appState);
|
||||
if (appState.paymentCompleted) {
|
||||
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
this.$root.$emit('habitica:payment-success', appState);
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
setupPayments();
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
|
||||
});
|
||||
} else {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('playSound');
|
||||
this.$root.$off('buyModal::showItem');
|
||||
this.$root.$off('selectMembersModal::showItem');
|
||||
},
|
||||
mounted () {
|
||||
// Remove the index.html loading screen and now show the inapp loading
|
||||
const loadingScreen = document.getElementById('loading-screen');
|
||||
if (loadingScreen) document.body.removeChild(loadingScreen);
|
||||
|
||||
if (this.isStaticPage || !this.isUserLoggedIn) {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
checkForBannedUser (error) {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
@@ -507,57 +296,10 @@ export default {
|
||||
this.$store.dispatch('auth:logout', { redirectToLogin: true });
|
||||
return true;
|
||||
},
|
||||
itemSelected (item) {
|
||||
this.selectedItemToBuy = item;
|
||||
},
|
||||
genericPurchase (item) {
|
||||
if (!item) return false;
|
||||
|
||||
if (['card', 'debuffPotion'].includes(item.purchaseType)) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
customPurchase (item) {
|
||||
if (item.purchaseType === 'card') {
|
||||
this.selectedSpellToBuy = item;
|
||||
|
||||
// hide the dialog
|
||||
this.$root.$emit('bv::hide::modal', 'buy-modal');
|
||||
// remove the dialog from our modal-stack,
|
||||
// the default hidden event is delayed
|
||||
this.$root.$emit('bv::modal::hidden', {
|
||||
target: {
|
||||
id: 'buy-modal',
|
||||
},
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'select-member-modal');
|
||||
}
|
||||
|
||||
if (item.purchaseType === 'debuffPotion') {
|
||||
this.castStart(item, this.user);
|
||||
}
|
||||
},
|
||||
async memberSelected (member) {
|
||||
await this.castStart(this.selectedSpellToBuy, member);
|
||||
|
||||
this.selectedSpellToBuy = null;
|
||||
|
||||
if (this.user.party._id) {
|
||||
this.$store.dispatch('party:getMembers', { forceLoad: true });
|
||||
}
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'select-member-modal');
|
||||
},
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style src="intro.js/minified/introjs.min.css"></style>
|
||||
<style src="axios-progress-bar/dist/nprogress.css"></style>
|
||||
<style src="@/assets/scss/index.scss" lang="scss"></style>
|
||||
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
|
||||
<style src="smartbanner.js/dist/smartbanner.min.css"></style>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
BIN
website/client/src/assets/images/group-plans-static/group-management@3x.png
Executable file → Normal file
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 26 KiB |
BIN
website/client/src/assets/images/group-plans-static/team-based@3x.png
Executable file → Normal file
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 410 B |
@@ -174,6 +174,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: $orange-10;
|
||||
color: $white !important;
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: $orange-100;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $orange-10;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
|
||||
box-shadow: none;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
|
||||
background: $orange-10;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: $green-50;
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -78,3 +78,15 @@ $gold-color: #FFA624;
|
||||
$hourglass-color: #2995CD;
|
||||
|
||||
$purple-task: #925cf3;
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
.purple-300 {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.white {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
@@ -23,16 +23,14 @@
|
||||
{{ $t('foundNewItems') }}
|
||||
</h2>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div
|
||||
<Sprite
|
||||
class="item-box ml-auto mr-3"
|
||||
:class="eggClass"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
:image-name="eggClass"
|
||||
/>
|
||||
<Sprite
|
||||
class="item-box mr-auto"
|
||||
:class="potionClass"
|
||||
>
|
||||
</div>
|
||||
:image-name="potionClass"
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
v-once
|
||||
@@ -103,8 +101,12 @@
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
</div>
|
||||
<div class="inner-content">
|
||||
<div class="achievement-background d-flex align-items-center">
|
||||
<div
|
||||
<Sprite
|
||||
class="icon"
|
||||
:class="achievementClass"
|
||||
></div>
|
||||
:image-name="achievementClass"
|
||||
/>
|
||||
</div>
|
||||
<h4
|
||||
class="title"
|
||||
@@ -99,8 +99,12 @@
|
||||
import achievements from '@/../../common/script/content/achievements';
|
||||
import { mapState } from '@/libs/store';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
props: ['data'],
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,30 +1,41 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<div class="well col-12">
|
||||
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||
<div class="admin-panel-content">
|
||||
<h1>Admin Panel</h1>
|
||||
|
||||
<div>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="loadHero(userIdentifier)"
|
||||
>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="searchUsers(userIdentifier)"
|
||||
>
|
||||
<div class="input-group col pl-0 pr-0">
|
||||
<input
|
||||
v-model="userIdentifier"
|
||||
class="form-control uidField"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="'User ID or Username; blank for your account'"
|
||||
:placeholder="'UserID, username, email, or leave blank for your account'"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Load User"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
@click="loadUser(userIdentifier)"
|
||||
>
|
||||
Load User
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
type="button"
|
||||
@click="searchUsers(userIdentifier)"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<router-view @changeUserIdentifier="changeUserIdentifier" />
|
||||
</div>
|
||||
<router-view
|
||||
class="mt-3"
|
||||
@changeUserIdentifier="changeUserIdentifier"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,6 +44,15 @@
|
||||
.uidField {
|
||||
min-width: 45ch;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
width:auto;
|
||||
}
|
||||
|
||||
.admin-panel-content {
|
||||
flex: 0 0 800px;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -62,7 +82,24 @@ export default {
|
||||
// (useful if we want to re-fetch the user after making changes).
|
||||
this.userIdentifier = newId;
|
||||
},
|
||||
async loadHero (userIdentifier) {
|
||||
async searchUsers (userIdentifier) {
|
||||
if (!userIdentifier || userIdentifier === '') {
|
||||
this.loadUser();
|
||||
return;
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'adminPanelSearch',
|
||||
params: { userIdentifier },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async loadUser (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
|
||||
this.$router.push({
|
||||
|
||||
155
website/client/src/components/admin-panel/search.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-if="noUsersFound"
|
||||
class="alert alert-warning"
|
||||
role="alert"
|
||||
>
|
||||
Could not find any matching users.
|
||||
</div>
|
||||
<loading-spinner class="mx-auto mb-2" dark-color="true" v-if="isSearching" />
|
||||
<div
|
||||
v-if="users.length > 0"
|
||||
class="list-group"
|
||||
>
|
||||
<a
|
||||
v-for="user in users"
|
||||
:key="user._id"
|
||||
href="#"
|
||||
class="list-group-item list-group-item-action"
|
||||
@click="loadUser(user._id)"
|
||||
>
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{ user.profile.name }}</h5>
|
||||
<small>{{ user._id }}</small>
|
||||
</div>
|
||||
<p
|
||||
class="mb-1"
|
||||
:class="{'highlighted-value': matchValueToIdentifier(user.auth.local.username)}"
|
||||
>
|
||||
@{{ user.auth.local.username }}</p>
|
||||
<p class="mb-0">
|
||||
<span
|
||||
v-for="email in userEmails(user)"
|
||||
:key="email"
|
||||
:class="{'highlighted-value': matchValueToIdentifier(email)}"
|
||||
>
|
||||
{{ email }}
|
||||
</span>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.highlighted-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import VueRouter from 'vue-router';
|
||||
import { mapState } from '@/libs/store';
|
||||
import LoadingSpinner from '../ui/loadingSpinner';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userIdentifier: '',
|
||||
users: [],
|
||||
noUsersFound: false,
|
||||
isSearching: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
beforeRouteUpdate (to, from, next) {
|
||||
this.userIdentifier = to.params.userIdentifier;
|
||||
next();
|
||||
},
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||
this.isSearching = false;
|
||||
if (users.length === 1) {
|
||||
this.loadUser(users[0]._id);
|
||||
} else {
|
||||
const matchIndex = users.findIndex(user => this.isExactMatch(user));
|
||||
if (matchIndex !== -1) {
|
||||
users.splice(0, 0, users.splice(matchIndex, 1)[0]);
|
||||
}
|
||||
this.users = users;
|
||||
this.noUsersFound = users.length === 0;
|
||||
}
|
||||
});
|
||||
this.$emit('changeUserIdentifier', this.userIdentifier); // change user identifier in Admin Panel's form
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.userIdentifier = this.$route.params.userIdentifier;
|
||||
},
|
||||
methods: {
|
||||
matchValueToIdentifier (value) {
|
||||
return value.toLowerCase().includes(this.userIdentifier.toLowerCase());
|
||||
},
|
||||
userEmails (user) {
|
||||
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;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.apple && user.auth.apple.emails) {
|
||||
const emails = user.auth.apple.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||
const emails = user.auth.facebook.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
return allEmails;
|
||||
},
|
||||
findSocialEmails (emails) {
|
||||
if (typeof emails === 'string') return [emails];
|
||||
if (Array.isArray(emails)) return emails.map(email => email.value);
|
||||
if (typeof emails === 'object') return [emails.value];
|
||||
return [];
|
||||
},
|
||||
async loadUser (userIdentifier) {
|
||||
const id = userIdentifier || this.user._id;
|
||||
|
||||
this.$router.push({
|
||||
name: 'adminPanelUser',
|
||||
params: { userIdentifier: id },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
// the admin has requested that the same user be displayed again so reload the page
|
||||
// (e.g., if they changed their mind about changes they were making)
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
},
|
||||
isExactMatch (user) {
|
||||
return user._id === this.userIdentifier
|
||||
|| user.auth.local.username === this.userIdentifier
|
||||
|| (user.auth.google && user.auth.google.emails && user.auth.google.emails.findIndex(
|
||||
email => email.value === this.userIdentifier,
|
||||
) !== -1)
|
||||
|| (user.auth.apple && user.auth.apple.emails && user.auth.apple.emails.findIndex(
|
||||
email => email.value === this.userIdentifier,
|
||||
) !== -1)
|
||||
|| (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails.findIndex(
|
||||
email => email.value === this.userIdentifier,
|
||||
) !== -1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Achievements
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Achievements
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in achievements"
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Current Avatar Appearance, Drop Count Today
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Current Avatar Appearance, Drop Count Today
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div>Drops Today: {{ items.lastDrop.count }}</div>
|
||||
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
|
||||
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>
|
||||
|
||||
@@ -1,160 +1,129 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Contributor Details
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Contributor details', clearData: true})">
|
||||
<div>
|
||||
<label>Permissions</label>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.fullAccess"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Full Admin Access (Allows access to everything. EVERYTHING)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.userSupport"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
User Support (Access this form, access purchase history)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.news"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
News poster (Bailey CMS)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.moderator"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Community Moderator (ban and mute users, access chat flags, manage social spaces)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.challengeAdmin"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Challenge Admin (can create official habitica challenges and admin all challenges)
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.permissions.coupons"
|
||||
:disabled="!hasPermission(user, 'fullAccess')"
|
||||
type="checkbox"
|
||||
>
|
||||
Coupon Creator (can manage coupon codes)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Title</label>
|
||||
<input
|
||||
v-model="hero.contributor.text"
|
||||
class="form-control textField"
|
||||
type="text"
|
||||
>
|
||||
<small>
|
||||
Common titles:
|
||||
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
|
||||
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
|
||||
<br>
|
||||
Rare titles:
|
||||
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
|
||||
Statistician, Tinker, Transcriber, Troubadour.
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group form-inline">
|
||||
<label>Tier</label>
|
||||
<input
|
||||
v-model="hero.contributor.level"
|
||||
class="form-control levelField"
|
||||
type="number"
|
||||
>
|
||||
<small>
|
||||
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
||||
This determines which items, pets, mounts are available, and name-tag coloring.
|
||||
Tiers 8 and 9 are automatically given admin status.
|
||||
</small>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.secret.text"
|
||||
class="form-group"
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Contributor details', clearData: true })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
<label>Moderation Notes</label>
|
||||
Contributor Details
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<h3 class="mt-0">
|
||||
Permissions
|
||||
</h3>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
v-for="permission in permissionList"
|
||||
:key="permission.key"
|
||||
class="col-sm-9 offset-sm-3"
|
||||
>
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
v-model="hero.permissions[permission.key]"
|
||||
:disabled="!hasPermission(user, permission.key)"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label class="custom-control-label">
|
||||
{{ permission.name }}<br>
|
||||
<small class="text-secondary">{{ permission.description }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Contributions</label>
|
||||
<textarea
|
||||
v-model="hero.contributor.contributions"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.contributor.contributions"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Title</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.contributor.text"
|
||||
class="form-control textField"
|
||||
type="text"
|
||||
>
|
||||
<small>
|
||||
Common titles:
|
||||
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
|
||||
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
|
||||
<br>
|
||||
Rare titles:
|
||||
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
|
||||
Statistician, Tinker, Transcriber, Troubadour.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Edit Moderation Notes</label>
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Tier</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.contributor.level"
|
||||
class="form-control levelField"
|
||||
type="number"
|
||||
>
|
||||
<small>
|
||||
1-7 for normal contributors, 8 for moderators, 9 for staff.
|
||||
This determines which items, pets, mounts are available, and name-tag coloring.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Contributions</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
v-model="hero.contributor.contributions"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
>
|
||||
</textarea>
|
||||
<div
|
||||
v-markdown="hero.contributor.contributions"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Moderation Notes</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save and Clear Data"
|
||||
class="btn btn-primary"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.levelField {
|
||||
min-width: 10ch;
|
||||
}
|
||||
.textField {
|
||||
min-width: 50ch;
|
||||
}
|
||||
.levelField {
|
||||
min-width: 10ch;
|
||||
}
|
||||
|
||||
.textField {
|
||||
min-width: 50ch;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -164,6 +133,39 @@ import saveHero from '../mixins/saveHero';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { userStateMixin } from '../../../mixins/userState';
|
||||
|
||||
const permissionList = [
|
||||
{
|
||||
key: 'fullAccess',
|
||||
name: 'Full Admin Access',
|
||||
description: 'Allows access to everything. EVERYTHING',
|
||||
},
|
||||
{
|
||||
key: 'userSupport',
|
||||
name: 'User Support',
|
||||
description: 'Access this form, access purchase history',
|
||||
},
|
||||
{
|
||||
key: 'news',
|
||||
name: 'News Poster',
|
||||
description: 'Bailey CMS',
|
||||
},
|
||||
{
|
||||
key: 'moderator',
|
||||
name: 'Community Moderator',
|
||||
description: 'Ban and mute users, access chat flags, manage social spaces',
|
||||
},
|
||||
{
|
||||
key: 'challengeAdmin',
|
||||
name: 'Challenge Admin',
|
||||
description: 'Can create official habitica challenges and admin all challenges',
|
||||
},
|
||||
{
|
||||
key: 'coupons',
|
||||
name: 'Coupon Creator',
|
||||
description: 'Can manage coupon codes',
|
||||
},
|
||||
];
|
||||
|
||||
function resetData (self) {
|
||||
self.expand = self.hero.contributor.level;
|
||||
}
|
||||
@@ -192,6 +194,7 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
permissionList,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -1,145 +1,187 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Timestamps, Time Zone, Authentication, Email Address
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Authentication' })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Timestamps, Time Zone, Authentication, Email Address
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
See error(s) below.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
Account created:
|
||||
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
|
||||
</div>
|
||||
<div v-if="hero.flags.thirdPartyTools">
|
||||
User has employed <strong>third party tools</strong>. Last known usage:
|
||||
<strong>{{ hero.flags.thirdPartyTools | formatDate }}</strong>
|
||||
</div>
|
||||
<div v-if="cronError">
|
||||
"lastCron" value:
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
<span class="errorMessage">
|
||||
ERROR: cron probably crashed before finishing
|
||||
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<div>
|
||||
Most recent cron:
|
||||
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
("auth.timestamps.loggedin")
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary ml-2"
|
||||
@click="resetCron()"
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</button>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Time zone:
|
||||
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Custom Day Start time (CDS):
|
||||
<strong>{{ hero.preferences.dayStart }}</strong>
|
||||
</div>
|
||||
<div v-if="timezoneDiffError || timezoneMissingError">
|
||||
Time zone at previous cron:
|
||||
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
|
||||
See error(s) below.
|
||||
</p>
|
||||
|
||||
<div class="errorMessage">
|
||||
<div v-if="timezoneDiffError">
|
||||
ERROR: the player's current time zone is different than their time zone when
|
||||
their previous cron ran. This can be because:
|
||||
<ul>
|
||||
<li>daylight savings started or stopped <sup>*</sup></li>
|
||||
<li>the player changed zones due to travel <sup>*</sup></li>
|
||||
<li>the player has devices set to different zones <sup>**</sup></li>
|
||||
<li>the player uses a VPN with varying zones <sup>**</sup></li>
|
||||
<li>something similarly unpleasant is happening. <sup>**</sup></li>
|
||||
</ul>
|
||||
<p>
|
||||
<em>* The problem should fix itself in about a day.</em><br>
|
||||
<em>** One of these causes is probably happening if the time zones stay
|
||||
different for more than a day.</em>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Account created:</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ hero.auth.timestamps.created | formatDate }}</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Used third party tools:</label>
|
||||
|
||||
<div v-if="timezoneMissingError">
|
||||
ERROR: One of the player's time zones is missing.
|
||||
This is expected and okay if it's the "Time zone at previous cron"
|
||||
AND if it's their first day in Habitica.
|
||||
Otherwise an error has occurred.
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<strong v-if="hero.flags.thirdPartyTools">
|
||||
Yes - {{ hero.flags.thirdPartyTools | formatDate }}</strong>
|
||||
<strong v-else>No</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subsection-start form-inline">
|
||||
API Token:
|
||||
<form @submit.prevent="changeApiToken()">
|
||||
<input
|
||||
type="submit"
|
||||
value="Change API Token"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
</form>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
class="form-inline"
|
||||
>
|
||||
<strong>API Token has been changed. Tell the player something like this:</strong>
|
||||
<div v-if="cronError" class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">lastCron value:</label>
|
||||
<strong>{{ hero.lastCron | formatDate }}</strong>
|
||||
<br>
|
||||
I've given you a new API Token.
|
||||
You'll need to log out of the website and mobile app then log back in
|
||||
otherwise they won't work correctly.
|
||||
If you have trouble logging out, for the website go to
|
||||
https://habitica.com/static/clear-browser-data and click the red button there,
|
||||
and for the Android app, clear its data.
|
||||
For the iOS app, if you can't log out you might need to uninstall it,
|
||||
reboot your phone, then reinstall it.
|
||||
<span class="errorMessage">
|
||||
ERROR: cron probably crashed before finishing
|
||||
("auth.timestamps.loggedin" and "lastCron" dates are different).
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Most recent cron:</label>
|
||||
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<strong>
|
||||
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
|
||||
<button
|
||||
class="btn btn-warning btn-sm ml-4"
|
||||
@click="resetCron()"
|
||||
>
|
||||
Reset Cron to Yesterday
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Time zone:</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Custom Day Start time (CDS)</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.preferences.dayStart"
|
||||
class="form-control levelField"
|
||||
type="number"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="timezoneDiffError || timezoneMissingError">
|
||||
Time zone at previous cron:
|
||||
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
|
||||
|
||||
<div class="errorMessage">
|
||||
<div v-if="timezoneDiffError">
|
||||
ERROR: the player's current time zone is different than their time zone when
|
||||
their previous cron ran. This can be because:
|
||||
<ul>
|
||||
<li>daylight savings started or stopped <sup>*</sup></li>
|
||||
<li>the player changed zones due to travel <sup>*</sup></li>
|
||||
<li>the player has devices set to different zones <sup>**</sup></li>
|
||||
<li>the player uses a VPN with varying zones <sup>**</sup></li>
|
||||
<li>something similarly unpleasant is happening. <sup>**</sup></li>
|
||||
</ul>
|
||||
<p>
|
||||
<em>* The problem should fix itself in about a day.</em><br>
|
||||
<em>** One of these causes is probably happening if the time zones stay
|
||||
different for more than a day.</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="timezoneMissingError">
|
||||
ERROR: One of the player's time zones is missing.
|
||||
This is expected and okay if it's the "Time zone at previous cron"
|
||||
AND if it's their first day in Habitica.
|
||||
Otherwise an error has occurred.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">API Token</label>
|
||||
<div class="col-sm-9">
|
||||
<button
|
||||
value="Change API Token"
|
||||
class="btn btn-danger"
|
||||
@click="changeApiToken()"
|
||||
>
|
||||
Change API Token
|
||||
</button>
|
||||
<div
|
||||
v-if="tokenModified"
|
||||
>
|
||||
<strong>API Token has been changed. Tell the player something like this:</strong>
|
||||
<br>
|
||||
I've given you a new API Token.
|
||||
You'll need to log out of the website and mobile app then log back in
|
||||
otherwise they won't work correctly.
|
||||
If you have trouble logging out, for the website go to
|
||||
https://habitica.com/static/clear-browser-data and click the red button there,
|
||||
and for the Android app, clear its data.
|
||||
For the iOS app, if you can't log out you might need to uninstall it,
|
||||
reboot your phone, then reinstall it.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Local Authentication E-Mail</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.auth.local.email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Google authentication</label>
|
||||
<div class="col-sm-9">
|
||||
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Facebook authentication</label>
|
||||
<div class="col-sm-9">
|
||||
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Apple ID authentication</label>
|
||||
<div class="col-sm-9">
|
||||
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Full "auth" object for checking above is correct:
|
||||
<pre>{{ hero.auth }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subsection-start">
|
||||
Local authentication:
|
||||
<span v-if="hero.auth.local.email">Yes,
|
||||
<strong>{{ hero.auth.local.email }}</strong></span>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Google authentication:
|
||||
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Facebook authentication:
|
||||
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div>
|
||||
Apple ID authentication:
|
||||
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
|
||||
<span v-else><strong>None</strong></span>
|
||||
</div>
|
||||
<div class="subsection-start">
|
||||
Full "auth" object for checking above is correct:
|
||||
<pre>{{ hero.auth }}</pre>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Customizations
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Customizations
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div
|
||||
v-for="itemType in itemTypes"
|
||||
:key="itemType"
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Items
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Items
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div>
|
||||
The sections below display each item's key (bolded if the player has ever owned it),
|
||||
followed by the item's English name.
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Party, Quest
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Party, Quest
|
||||
<span
|
||||
v-if="errorsOrWarningsExist"
|
||||
>- ERRORS / WARNINGS EXIST</span>
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<div
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
|
||||
@@ -1,87 +1,132 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Privileges, Gem Balance
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Priviliges, Gem Balance
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Player has had privileges removed or has moderation notes.
|
||||
</p>
|
||||
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-if="hero.flags"
|
||||
v-model="hero.flags.chatShadowMuted"
|
||||
type="checkbox"
|
||||
> Shadow Mute
|
||||
</label>
|
||||
<p
|
||||
v-if="errorsOrWarningsExist"
|
||||
class="errorMessage"
|
||||
>
|
||||
Player has had privileges removed or has moderation notes.
|
||||
</p>
|
||||
<div
|
||||
v-if="hero.flags"
|
||||
class="form-group row"
|
||||
>
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="chatShadowMuted"
|
||||
v-model="hero.flags.chatShadowMuted"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="chatShadowMuted"
|
||||
>
|
||||
Shadow Mute
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-if="hero.flags"
|
||||
v-model="hero.flags.chatRevoked"
|
||||
type="checkbox"
|
||||
> Mute (Revoke Chat Privileges)
|
||||
</label>
|
||||
<div
|
||||
v-if="hero.flags"
|
||||
class="form-group row"
|
||||
>
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="chatRevoked"
|
||||
v-model="hero.flags.chatRevoked"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="chatRevoked"
|
||||
>
|
||||
Mute (Revoke Chat Privileges)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="hero.auth.blocked"
|
||||
type="checkbox"
|
||||
> Ban / Block
|
||||
</label>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="blocked"
|
||||
v-model="hero.auth.blocked"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="blocked"
|
||||
>
|
||||
Ban / Block
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Balance
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.balance"
|
||||
class="form-control balanceField"
|
||||
type="number"
|
||||
step="0.25"
|
||||
>
|
||||
</label>
|
||||
<span>
|
||||
<small>
|
||||
Balance is in USD, not in Gems.
|
||||
E.g., if this number is 1, it means 4 Gems.
|
||||
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
|
||||
Do not use when awarding tiers; tier gems are automatic.
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Moderation Notes</label>
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Moderation Notes</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
v-model="hero.secret.text"
|
||||
class="form-control"
|
||||
cols="5"
|
||||
rows="5"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.secret.text"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{ 'open': expand }"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div v-if="hero.purchased.plan.paymentMethod">
|
||||
Payment method:
|
||||
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
|
||||
@@ -23,46 +28,72 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.dateCreated"
|
||||
class="form-inline"
|
||||
class="form-group row"
|
||||
>
|
||||
<label>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Creation date:
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateCreated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateCreated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateCreated) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.dateCurrentTypeCreated"
|
||||
class="form-inline"
|
||||
class="form-group row"
|
||||
>
|
||||
<label>
|
||||
Start date for current subscription type:
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Current sub start date:
|
||||
</label>
|
||||
<strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateCurrentTypeCreated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Termination date:
|
||||
<div>
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="hero.purchased.plan.dateTerminated"
|
||||
class="form-control"
|
||||
type="text"
|
||||
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateTerminated) }}</strong>
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<strong class="input-group-text">
|
||||
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Consecutive months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.count"
|
||||
class="form-control"
|
||||
@@ -70,11 +101,13 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Perk offset months:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.offset"
|
||||
class="form-control"
|
||||
@@ -82,26 +115,34 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Perk month count:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.perkMonthCount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
max="2"
|
||||
step="1"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
Perk month count:
|
||||
<input
|
||||
v-model="hero.purchased.plan.perkMonthCount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
max="2"
|
||||
step="1"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Next Mystic Hourglass:
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">{{ nextHourglassDate }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Next Mystic Hourglass:
|
||||
<strong>{{ nextHourglassDate }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystic Hourglasses:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||
class="form-control"
|
||||
@@ -109,11 +150,13 @@
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Gem cap increase:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
||||
class="form-control"
|
||||
@@ -122,15 +165,21 @@
|
||||
max="25"
|
||||
step="5"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Total Gem cap:
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
Total Gem cap:
|
||||
<strong>{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Gems bought this month:
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
class="form-control"
|
||||
@@ -139,43 +188,64 @@
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.extraMonths > 0"
|
||||
>
|
||||
<div v-if="hero.purchased.plan.extraMonths > 0">
|
||||
Additional credit (applied upon cancellation):
|
||||
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Mystery Items:
|
||||
<span
|
||||
v-if="hero.purchased.plan.mysteryItems.length > 0"
|
||||
>
|
||||
<span
|
||||
v-for="(item, index) in hero.purchased.plan.mysteryItems"
|
||||
:key="index"
|
||||
>
|
||||
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
|
||||
{{ item }},
|
||||
</strong>
|
||||
<strong v-else> {{ item }} </strong>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">
|
||||
Mystery Items:
|
||||
</label>
|
||||
<div class="col-sm-9 col-form-label">
|
||||
<span v-if="hero.purchased.plan.mysteryItems.length > 0">
|
||||
<span
|
||||
v-for="(item, index) in hero.purchased.plan.mysteryItems"
|
||||
:key="index"
|
||||
>
|
||||
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
|
||||
{{ item }},
|
||||
</strong>
|
||||
<strong v-else> {{ item }} </strong>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<strong>None</strong>
|
||||
</span>
|
||||
<span v-else>
|
||||
<strong>None</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { getPlanContext } from '@/../../common/script/cron';
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleTransactionsOpen"
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="toggleTransactionsOpen"
|
||||
>
|
||||
Transactions
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
Transactions
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<purchase-history-table
|
||||
:gem-transactions="gemTransactions"
|
||||
:hourglass-transactions="hourglassTransactions"
|
||||
|
||||
@@ -1,52 +1,66 @@
|
||||
<template>
|
||||
<div class="accordion-group">
|
||||
<h3
|
||||
class="expand-toggle"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
Users Profile
|
||||
</h3>
|
||||
<div v-if="expand">
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||
<div class="form-group">
|
||||
<label>Display name</label>
|
||||
<input
|
||||
v-model="hero.profile.name"
|
||||
class="form-control textField"
|
||||
type="text"
|
||||
>
|
||||
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
User Profile
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Display name</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.profile.name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Photo URL</label>
|
||||
<input
|
||||
v-model="hero.profile.imageUrl"
|
||||
class="form-control textField"
|
||||
type="text"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">Photo URL</label>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="hero.profile.imageUrl"
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>About</label>
|
||||
<div class="row about-row">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label">About</label>
|
||||
<div class="col-sm-9">
|
||||
<textarea
|
||||
v-model="hero.profile.blurb"
|
||||
class="form-control col"
|
||||
class="form-control"
|
||||
rows="10"
|
||||
></textarea>
|
||||
<div
|
||||
v-markdown="hero.profile.blurb"
|
||||
class="markdownPreview col"
|
||||
class="markdownPreview"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-footer"
|
||||
>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save"
|
||||
class="btn btn-primary"
|
||||
class="btn btn-primary mt-1"
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<buy-gems-modal v-if="user" />
|
||||
<!--modify-inventory(v-if="isUserLoaded")-->
|
||||
<footer>
|
||||
<!-- Product -->
|
||||
<div class="product">
|
||||
@@ -22,7 +21,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/group-plans">
|
||||
<router-link :to="user ? '/group-plans' : '/static/group-plans'">
|
||||
{{ $t('groupPlans') }}
|
||||
</router-link>
|
||||
</li>
|
||||
@@ -291,7 +290,8 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
|
||||
class="time-travel"
|
||||
v-if="TIME_TRAVEL_ENABLED && user?.permissions?.fullAccess"
|
||||
:key="lastTimeJump"
|
||||
>
|
||||
<a
|
||||
@@ -309,9 +309,11 @@
|
||||
<div class="my-2">
|
||||
Time Traveling! It is {{ new Date().toLocaleDateString() }}
|
||||
<a
|
||||
class="btn btn-warning mr-1"
|
||||
class="btn btn-warning btn-small"
|
||||
@click="resetTime()"
|
||||
>Reset</a>
|
||||
>
|
||||
Reset
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="btn btn-secondary mr-1"
|
||||
@@ -399,6 +401,10 @@
|
||||
tooltip="+1000 to boss quests. 300 items to collection quests"
|
||||
@click="addQuestProgress()"
|
||||
>Quest Progress Up</a>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
@click="bossRage()"
|
||||
>+ Boss Rage 😡</a>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
@click="makeAdmin()"
|
||||
@@ -506,6 +512,8 @@ li {
|
||||
grid-area: debug-pop;
|
||||
}
|
||||
|
||||
.time-travel { grid-area: time-travel;}
|
||||
|
||||
footer {
|
||||
background-color: $gray-500;
|
||||
color: $gray-50;
|
||||
@@ -526,7 +534,8 @@ footer {
|
||||
"donate-text donate-text donate-text donate-button social"
|
||||
"hr hr hr hr hr"
|
||||
"copyright copyright melior privacy-terms privacy-terms"
|
||||
"debug-toggle debug-toggle debug-toggle blank blank";
|
||||
"time-travel time-travel time-travel time-travel time-travel"
|
||||
"debug-toggle debug-toggle debug-toggle debug-toggle debug-toggle";
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: auto;
|
||||
|
||||
@@ -730,6 +739,7 @@ h3 {
|
||||
"privacy-policy privacy-policy"
|
||||
"mobile-terms mobile-terms"
|
||||
"melior melior"
|
||||
"time-travel time-travel"
|
||||
"debug-toggle debug-toggle";
|
||||
grid-template-columns: repeat(2, 2fr);
|
||||
grid-template-rows: auto;
|
||||
@@ -960,6 +970,10 @@ export default {
|
||||
// @TODO: Notification.text('Quest progress increased');
|
||||
// @TODO: User.sync();
|
||||
},
|
||||
async bossRage () {
|
||||
await axios.post('/api/v4/debug/boss-rage');
|
||||
},
|
||||
|
||||
async makeAdmin () {
|
||||
await axios.post('/api/v4/debug/make-admin');
|
||||
// @TODO: Notification.text('You are now an admin!
|
||||
|
||||
@@ -224,7 +224,7 @@
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
|
||||
import googleIcon from '@/assets/svg/google.svg';
|
||||
|
||||
@@ -607,11 +607,10 @@
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import DOMPurify from 'dompurify';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
import exclamation from '@/assets/svg/exclamation.svg';
|
||||
import gryphon from '@/assets/svg/gryphon.svg';
|
||||
import habiticaIcon from '@/assets/svg/logo-horizontal.svg';
|
||||
@@ -619,6 +618,7 @@ import googleIcon from '@/assets/svg/google.svg';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg';
|
||||
|
||||
export default {
|
||||
mixins: [sanitizeRedirect],
|
||||
data () {
|
||||
const data = {
|
||||
username: '',
|
||||
@@ -747,11 +747,6 @@ export default {
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) return '/';
|
||||
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
return sanitizedString;
|
||||
},
|
||||
async register () {
|
||||
// @TODO do not use alert
|
||||
if (!this.email) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<div
|
||||
v-for="option in items"
|
||||
:key="option.key"
|
||||
:id="option.imageName"
|
||||
class="outer-option-background"
|
||||
:class="{
|
||||
premium: Boolean(option.gem),
|
||||
@@ -14,18 +15,28 @@
|
||||
hide: option.hide }"
|
||||
@click="option.click(option)"
|
||||
>
|
||||
<b-popover
|
||||
:target="option.imageName"
|
||||
triggers="hover focus"
|
||||
placement="bottom"
|
||||
:prevent-overflow="false"
|
||||
>
|
||||
<strong> {{ option.text }} </strong>
|
||||
</b-popover>
|
||||
<div class="option">
|
||||
<div
|
||||
class="sprite customize-option"
|
||||
:class="option.class"
|
||||
>
|
||||
<Sprite
|
||||
v-if="!option.none"
|
||||
class="sprite"
|
||||
:prefix="option.isGear ? 'shop' : 'icon'"
|
||||
:imageName="option.imageName"
|
||||
:image-name="option.imageName"
|
||||
/>
|
||||
<div
|
||||
v-if="option.none"
|
||||
v-else
|
||||
class="redline-outer"
|
||||
>
|
||||
<div class="redline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,8 +46,12 @@
|
||||
import gem from '@/assets/svg/gem.svg';
|
||||
import gold from '@/assets/svg/gold.svg';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
import Sprite from '@/components/ui/sprite.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
mixins: [
|
||||
avatarEditorUtilities,
|
||||
],
|
||||
@@ -75,7 +90,7 @@ export default {
|
||||
cursor: pointer;
|
||||
|
||||
&.premium {
|
||||
height: 112px;
|
||||
height: 120px;
|
||||
width: 96px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
@@ -92,21 +107,9 @@ export default {
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
background-color: $white;
|
||||
|
||||
.sprite.customize-option.shirt {
|
||||
margin-left: -3px !important;
|
||||
// otherwise its overriden by the .outer-option-background:not(.none) { rules
|
||||
}
|
||||
|
||||
.sprite.customize-option.skin {
|
||||
margin-left: -8px !important;
|
||||
// otherwise its overriden by the .outer-option-background:not(.none) { rules
|
||||
}
|
||||
|
||||
.option {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding-left: 6px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -132,14 +135,14 @@ export default {
|
||||
}
|
||||
|
||||
.redline-outer {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
height: 68px;
|
||||
width: 68px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin: 0 auto 0 0;
|
||||
|
||||
.redline {
|
||||
width: 60px;
|
||||
width: 68px;
|
||||
height: 4px;
|
||||
display: block;
|
||||
background: red;
|
||||
@@ -148,7 +151,6 @@ export default {
|
||||
top: 0;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,10 +166,9 @@ export default {
|
||||
}
|
||||
.option {
|
||||
vertical-align: bottom;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
height: 76px;
|
||||
width: 76px;
|
||||
|
||||
margin: 12px 8px;
|
||||
border: 4px solid transparent;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
@@ -182,44 +183,6 @@ export default {
|
||||
.sprite.customize-option {
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
|
||||
&.skin {
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
}
|
||||
&.chair {
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
|
||||
&.button_chair_black {
|
||||
// different sprite margin?
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
&.handleless {
|
||||
margin-left: -5px;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
&.color, &.bangs, &.beard, &.flower, &.mustache {
|
||||
background-position-x: -6px;
|
||||
background-position-y: -12px;
|
||||
}
|
||||
|
||||
&.hair.base {
|
||||
background-position-x: -6px;
|
||||
background-position-y: -4px;
|
||||
}
|
||||
|
||||
&.headAccessory {
|
||||
margin-top: 0;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
&.headband {
|
||||
margin-top: -6px;
|
||||
margin-left: -27px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
|
||||
<script>
|
||||
import appearance from '@/../../common/script/content/appearance';
|
||||
import upperFirst from 'lodash/upperFirst';
|
||||
import { subPageMixin } from '../../mixins/subPage';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
|
||||
@@ -82,9 +83,6 @@ import customizeBanner from './customize-banner';
|
||||
import customizeOptions from './customize-options';
|
||||
import subMenu from './sub-menu';
|
||||
|
||||
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
|
||||
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
customizeBanner,
|
||||
@@ -106,17 +104,6 @@ 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'],
|
||||
specialShirtKeys,
|
||||
items: [
|
||||
{
|
||||
id: 'size',
|
||||
label: this.$t('size'),
|
||||
},
|
||||
{
|
||||
id: 'shirt',
|
||||
label: this.$t('shirt'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -167,6 +154,7 @@ export default {
|
||||
];
|
||||
const noneOption = this.createGearItem(0, 'eyewear', 'base');
|
||||
noneOption.none = true;
|
||||
noneOption.text = this.$t('none');
|
||||
const options = [
|
||||
noneOption,
|
||||
];
|
||||
@@ -178,42 +166,36 @@ export default {
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume.eyewear === newKey
|
||||
: this.user.items.gear.equipped.eyewear === newKey;
|
||||
option.class = `eyewear_special_${key}`;
|
||||
option.imageName = `eyewear_special_${key}`;
|
||||
option.isGear = true;
|
||||
option.click = () => {
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
option.text = this.$t(`eyewearSpecial${upperFirst(key)}Text`);
|
||||
options.push(option);
|
||||
}
|
||||
|
||||
return options;
|
||||
},
|
||||
freeShirts () {
|
||||
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
|
||||
},
|
||||
specialShirts () {
|
||||
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
|
||||
const keys = this.specialShirtKeys;
|
||||
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
|
||||
return options;
|
||||
},
|
||||
headbands () {
|
||||
const keys = ['blackHeadband', 'blueHeadband', 'greenHeadband', 'pinkHeadband', 'redHeadband', 'whiteHeadband', 'yellowHeadband'];
|
||||
const noneOption = this.createGearItem(0, 'headAccessory', 'base', 'headband');
|
||||
const noneOption = this.createGearItem(0, 'headAccessory', 'base');
|
||||
noneOption.none = true;
|
||||
noneOption.text = this.$t('none');
|
||||
const options = [
|
||||
noneOption,
|
||||
];
|
||||
|
||||
for (const key of keys) {
|
||||
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
|
||||
const option = this.createGearItem(key, 'headAccessory', 'special');
|
||||
const newKey = `headAccessory_special_${key}`;
|
||||
option.click = () => {
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
};
|
||||
|
||||
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
|
||||
options.push(option);
|
||||
}
|
||||
|
||||
@@ -227,8 +209,9 @@ export default {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.chair === key;
|
||||
option.class = `button_chair_${key} chair ${key.includes('handleless_') ? 'handleless' : ''}`;
|
||||
option.imageName = `chair_${key}`;
|
||||
option.click = () => this.set({ 'preferences.chair': key });
|
||||
option.text = appearance.chair[key].text();
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
@@ -242,8 +225,11 @@ export default {
|
||||
option.none = true;
|
||||
}
|
||||
option.active = this.user.preferences.hair.flower === key;
|
||||
option.class = `icon_hair_flower_${key} flower`;
|
||||
if (key !== 0) {
|
||||
option.imageName = `hair_flower_${key}`;
|
||||
}
|
||||
option.click = () => this.set({ 'preferences.hair.flower': key });
|
||||
option.text = appearance.hair.flower[key].text();
|
||||
return option;
|
||||
});
|
||||
return options;
|
||||
@@ -271,6 +257,7 @@ export default {
|
||||
|
||||
const noneOption = this.createGearItem(0, category, 'base', category);
|
||||
noneOption.none = true;
|
||||
noneOption.text = this.$t('none');
|
||||
const options = [
|
||||
noneOption,
|
||||
];
|
||||
@@ -284,10 +271,15 @@ export default {
|
||||
option.active = this.user.preferences.costume
|
||||
? this.user.items.gear.costume[category] === newKey
|
||||
: this.user.items.gear.equipped[category] === newKey;
|
||||
option.class = `headAccessory_special_${option.key} ${category}`;
|
||||
|
||||
if (category === 'back') {
|
||||
option.class = `icon_back_special_${option.key} back`;
|
||||
option.text = this.$t(`back${upperFirst(key)}Text`);
|
||||
option.imageName = `back_special_${option.key}`;
|
||||
} else {
|
||||
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
|
||||
option.imageName = `headAccessory_special_${option.key}`;
|
||||
}
|
||||
option.isGear = true;
|
||||
option.click = () => {
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.equip(newKey, type);
|
||||
@@ -303,7 +295,7 @@ export default {
|
||||
|
||||
return keys.join(',');
|
||||
},
|
||||
createGearItem (key, gearType, subGearType, additionalClass) {
|
||||
createGearItem (key, gearType, subGearType) {
|
||||
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
|
||||
const option = {};
|
||||
option.key = key;
|
||||
@@ -311,6 +303,7 @@ export default {
|
||||
const currentlyEquippedValue = this.user.items.gear[visibleGearType][gearType];
|
||||
|
||||
option.active = currentlyEquippedValue === newKey;
|
||||
option.isGear = true;
|
||||
|
||||
if (key === 0) {
|
||||
// if key is the "none" option check if a property
|
||||
@@ -318,7 +311,7 @@ export default {
|
||||
option.active = option.active || !currentlyEquippedValue;
|
||||
}
|
||||
|
||||
option.class = `${newKey} ${additionalClass}`;
|
||||
option.imageName = `${newKey}`;
|
||||
option.click = () => {
|
||||
const type = this.user.preferences.costume ? 'costume' : 'equipped';
|
||||
const currentlyEquipped = this.user.items.gear[type][gearType];
|
||||
|
||||
@@ -167,7 +167,7 @@ label {
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import closeX from '@/components/ui/closeX';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { MODALS } from '@/libs/consts';
|
||||
|
||||
@@ -220,10 +220,10 @@
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
<Sprite
|
||||
class="background"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
:image-name="`icon_background_${bg.key}`"
|
||||
/>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
@@ -254,10 +254,10 @@
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
<Sprite
|
||||
class="background"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
:image-name="`icon_background_${bg.key}`"
|
||||
/>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
@@ -286,10 +286,10 @@
|
||||
:class="{selected: bg.key === user.preferences.background}"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
<Sprite
|
||||
class="background"
|
||||
:class="`icon_background_${bg.key}`"
|
||||
></div>
|
||||
:image-name="`icon_background_${bg.key}`"
|
||||
/>
|
||||
<b-popover
|
||||
:target="bg.key"
|
||||
triggers="hover focus"
|
||||
@@ -818,9 +818,10 @@
|
||||
|
||||
.background {
|
||||
border-radius: 4px;
|
||||
object-position: -4px -4px;
|
||||
object-fit: none;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-position: -4px -4px;
|
||||
}
|
||||
|
||||
.deselect {
|
||||
@@ -1013,6 +1014,7 @@ import arrowRight from '@/assets/svg/arrow_right.svg';
|
||||
import arrowLeft from '@/assets/svg/arrow_left.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
|
||||
import Sprite from './ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -1024,6 +1026,7 @@ export default {
|
||||
hairSettings,
|
||||
skinSettings,
|
||||
usernameForm,
|
||||
Sprite,
|
||||
},
|
||||
mixins: [guide, notifications, avatarEditorUtilities],
|
||||
data () {
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
<!-- THIS IS A VERY OLD FILE DO NOT USE -->
|
||||
<template>
|
||||
<div class="create-group-modal-pages">
|
||||
<div
|
||||
v-if="activePage === PAGES.CREATE_GROUP"
|
||||
class="col-12"
|
||||
>
|
||||
<h2>{{ $t('nameYourGroup') }}</h2>
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="control-label"
|
||||
for="new-group-name"
|
||||
>{{ $t('name') }}</label>
|
||||
<input
|
||||
id="new-group-name"
|
||||
v-model="newGroup.name"
|
||||
class="form-control input-medium option-content"
|
||||
required="required"
|
||||
type="text"
|
||||
:placeholder="$t('exampleGroupName')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-description">{{ $t('description') }}</label>
|
||||
<textarea
|
||||
id="new-group-description"
|
||||
v-model="newGroup.description"
|
||||
class="form-control option-content"
|
||||
cols="3"
|
||||
:placeholder="$t('exampleGroupDesc')"
|
||||
></textarea>
|
||||
</div>
|
||||
<div
|
||||
v-if="newGroup.type === 'guild'"
|
||||
class="form-group text-left"
|
||||
>
|
||||
<div class="custom-control custom-radio">
|
||||
<input
|
||||
v-model="newGroup.privacy"
|
||||
class="custom-control-input"
|
||||
type="radio"
|
||||
name="new-group-privacy"
|
||||
value="private"
|
||||
>
|
||||
<label class="custom-control-label">{{ $t('thisGroupInviteOnly') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group text-left">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input
|
||||
id="create-group-leaderOnlyChallenges-checkbox"
|
||||
v-model="newGroup.leaderOnly.challenges"
|
||||
class="custom-control-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="custom-control-label"
|
||||
for="create-group-leaderOnlyChallenges-checkbox"
|
||||
>{{ $t('leaderOnlyChallenges') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="newGroup.type === 'party'"
|
||||
class="form-group"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary form-control"
|
||||
:value="$t('create')"
|
||||
@click="createGroup()"
|
||||
></button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary btn-lg btn-block"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
>
|
||||
{{ $t('create') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="activePage === PAGES.PAY"
|
||||
class="col-12"
|
||||
>
|
||||
<h2>{{ $t('choosePaymentMethod') }}</h2>
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h2 {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: 29px;
|
||||
color: #34313a;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
vertical-align: bottom;
|
||||
height: 100px;
|
||||
width: 306px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.box .svg-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.box:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
paymentsButtons,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
activePage: 'create-group',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
name: '',
|
||||
leaderOnly: {
|
||||
challenges: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
newGroupIsReady () {
|
||||
return Boolean(this.newGroup.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changePage (page) {
|
||||
this.activePage = page;
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly';
|
||||
const paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
};
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.redirectToStripe(paymentData);
|
||||
} else if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,16 +1,13 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="create-group"
|
||||
:title="activePage === PAGES.CREATE_GROUP ? 'Create your Group' : 'Select Payment'"
|
||||
:title="$t('createGroupTitle')"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
size="md"
|
||||
@hide="onHide()"
|
||||
>
|
||||
<div
|
||||
v-if="activePage === PAGES.CREATE_GROUP"
|
||||
class="col-12"
|
||||
>
|
||||
<div class="col-12">
|
||||
<!-- HEADER -->
|
||||
<div
|
||||
class="modal-close"
|
||||
@@ -25,7 +22,7 @@
|
||||
class="btn btn-primary next-button"
|
||||
:value="$t('next')"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
@click="stripeGroup({ group: newGroup })"
|
||||
>
|
||||
{{ $t('next') }}
|
||||
</button>
|
||||
@@ -101,25 +98,12 @@
|
||||
<button
|
||||
class="btn btn-primary btn-lg btn-block btn-payment"
|
||||
:disabled="!newGroupIsReady"
|
||||
@click="createGroup()"
|
||||
@click="stripeGroup({ group: newGroup })"
|
||||
>
|
||||
{{ $t('nextPaymentMethod') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PAYMENT -->
|
||||
<!-- @TODO: Separate payment into a separate modal -->
|
||||
<div
|
||||
v-if="activePage === PAGES.PAY"
|
||||
class="col-12 payments"
|
||||
>
|
||||
<div class="text-center">
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
@@ -195,9 +179,6 @@
|
||||
width: 200px;
|
||||
height: 215px;
|
||||
|
||||
.dollar {
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 60px;
|
||||
}
|
||||
@@ -248,31 +229,17 @@
|
||||
<script>
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
paymentsButtons,
|
||||
selectTranslatedArray,
|
||||
lockableLabel,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
// UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
paymentMethod: '',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
@@ -284,7 +251,6 @@ export default {
|
||||
demographics: null,
|
||||
user: '',
|
||||
},
|
||||
activePage: 'create-group',
|
||||
type: 'guild',
|
||||
};
|
||||
},
|
||||
@@ -302,55 +268,9 @@ export default {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'create-group');
|
||||
},
|
||||
changePage (page) {
|
||||
this.activePage = page;
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
|
||||
const demographicsKey = this.newGroup.demographics;
|
||||
const paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
demographics: demographicsKey,
|
||||
};
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan create',
|
||||
eventAction: 'group plan create',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.newGroup.demographics,
|
||||
type: this.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.redirectToStripe(paymentData);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
onHide () {
|
||||
this.sendingInProgress = false;
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="group-plan-overview"
|
||||
title="Empty"
|
||||
size="lg"
|
||||
hide-footer="hide-footer"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
class="header-wrap text-center"
|
||||
>
|
||||
<h2 v-once>
|
||||
{{ $t('gettingStarted') }}
|
||||
</h2>
|
||||
<p v-once>
|
||||
{{ $t('congratsOnGroupPlan') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="card"
|
||||
:class="{expanded: expandedQuestions.question1}"
|
||||
>
|
||||
<div class="question-head">
|
||||
<div class="q">
|
||||
Q.
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ $t('whatsIncludedGroup') }}
|
||||
</div>
|
||||
<div
|
||||
class="arrow float-right"
|
||||
@click="toggle('question1')"
|
||||
>
|
||||
<div
|
||||
v-if="expandedQuestions.question1"
|
||||
class="svg-icon"
|
||||
v-html="icons.upIcon"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
v-html="icons.downIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedQuestions.question1"
|
||||
class="question-body"
|
||||
>
|
||||
<p>{{ $t('whatsIncludedGroupDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="card"
|
||||
:class="{expanded: expandedQuestions.question2}"
|
||||
>
|
||||
<div class="question-head">
|
||||
<div class="q">
|
||||
Q.
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ $t('howDoesBillingWork') }}
|
||||
</div>
|
||||
<div
|
||||
class="arrow float-right"
|
||||
@click="toggle('question2')"
|
||||
>
|
||||
<div
|
||||
v-if="expandedQuestions.question2"
|
||||
class="svg-icon"
|
||||
v-html="icons.upIcon"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
v-html="icons.downIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedQuestions.question2"
|
||||
class="question-body"
|
||||
>
|
||||
<p>{{ $t('howDoesBillingWorkDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="card"
|
||||
:class="{expanded: expandedQuestions.question3}"
|
||||
>
|
||||
<div class="question-head">
|
||||
<div class="q">
|
||||
Q.
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ $t('howToAssignTask') }}
|
||||
</div>
|
||||
<div
|
||||
class="arrow float-right"
|
||||
@click="toggle('question3')"
|
||||
>
|
||||
<div
|
||||
v-if="expandedQuestions.question3"
|
||||
class="svg-icon"
|
||||
v-html="icons.upIcon"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
v-html="icons.downIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedQuestions.question3"
|
||||
class="question-body"
|
||||
>
|
||||
<p>{{ $t('howToAssignTaskDesc') }}</p>
|
||||
<div class="assign-tasks image-example"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="card"
|
||||
:class="{expanded: expandedQuestions.question4}"
|
||||
>
|
||||
<div class="question-head">
|
||||
<div class="q">
|
||||
Q.
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ $t('howToRequireApproval') }}
|
||||
</div>
|
||||
<div
|
||||
class="arrow float-right"
|
||||
@click="toggle('question4')"
|
||||
>
|
||||
<div
|
||||
v-if="expandedQuestions.question4"
|
||||
class="svg-icon"
|
||||
v-html="icons.upIcon"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
v-html="icons.downIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedQuestions.question4"
|
||||
class="question-body"
|
||||
>
|
||||
<p>{{ $t('howToRequireApprovalDesc') }}</p>
|
||||
<div class="requires-approval image-example"></div>
|
||||
<p>{{ $t('howToRequireApprovalDesc2') }}</p>
|
||||
<div class="approval-requested image-example"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="card"
|
||||
:class="{expanded: expandedQuestions.question5}"
|
||||
>
|
||||
<div class="question-head">
|
||||
<div class="q">
|
||||
Q.
|
||||
</div>
|
||||
<div class="title">
|
||||
{{ $t('whatIsGroupManager') }}
|
||||
</div>
|
||||
<div
|
||||
class="arrow float-right"
|
||||
@click="toggle('question5')"
|
||||
>
|
||||
<div
|
||||
v-if="expandedQuestions.question5"
|
||||
class="svg-icon"
|
||||
v-html="icons.upIcon"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
v-html="icons.downIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expandedQuestions.question5"
|
||||
class="question-body"
|
||||
>
|
||||
<p>{{ $t('whatIsGroupManagerDesc') }}</p>
|
||||
<div class="promote-leader image-example"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary close-button"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('goToTaskBoard') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#group-plan-overview___BV_modal_header_ {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
|
||||
|
||||
.header-wrap {
|
||||
padding-left: 4em;
|
||||
padding-right: 4em;
|
||||
|
||||
h2 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #878190;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-bottom: 2em;
|
||||
|
||||
.col-12 {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.card.expanded {
|
||||
padding-bottom: 1em;
|
||||
|
||||
.title {
|
||||
color: #4f2a93;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
min-height: 60px;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
.question-head {
|
||||
.q {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-size: 20px;
|
||||
color: #a5a1ac;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin: 1em;
|
||||
padding-top: .9em;
|
||||
|
||||
.svg-icon {
|
||||
width: 26px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.question-body {
|
||||
padding-left: 4.4em;
|
||||
padding-right: 4em;
|
||||
|
||||
p {
|
||||
color: #4e4a57;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-example {
|
||||
background-repeat: no-repeat;
|
||||
margin: 0 auto;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.assign-tasks {
|
||||
background-image: url('~@/assets/images/group-plans/assign-task@3x.png');
|
||||
width: 400px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.requires-approval {
|
||||
background-image: url('~@/assets/images/group-plans/requires-approval@3x.png');
|
||||
width: 402px;
|
||||
height: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.approval-requested {
|
||||
background-image: url('~@/assets/images/group-plans/approval-requested@3x.png');
|
||||
width: 471px;
|
||||
height: 204px;
|
||||
}
|
||||
|
||||
.promote-leader {
|
||||
background-image: url('~@/assets/images/group-plans/promote-leader@3x.png');
|
||||
width: 423px;
|
||||
height: 185px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import upIcon from '@/assets/svg/up.svg';
|
||||
import downIcon from '@/assets/svg/down.svg';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
upIcon,
|
||||
downIcon,
|
||||
}),
|
||||
expandedQuestions: {
|
||||
question1: false,
|
||||
question2: false,
|
||||
question3: false,
|
||||
question4: false,
|
||||
question5: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
toggle (question) {
|
||||
this.expandedQuestions[question] = !this.expandedQuestions[question];
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'group-plan-overview');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,7 +3,6 @@
|
||||
class="standard-page"
|
||||
@click="openCreateBtn ? openCreateBtn = false : null"
|
||||
>
|
||||
<group-plan-overview-modal />
|
||||
<task-modal
|
||||
ref="taskModal"
|
||||
:task="workingTask"
|
||||
@@ -187,7 +186,6 @@ import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import TaskColumn from '../tasks/column';
|
||||
import TaskModal from '../tasks/taskModal';
|
||||
import TaskSummary from '../tasks/taskSummary';
|
||||
import GroupPlanOverviewModal from './groupPlanOverviewModal';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
|
||||
import sync from '../../mixins/sync';
|
||||
@@ -208,7 +206,6 @@ export default {
|
||||
TaskColumn,
|
||||
TaskModal,
|
||||
TaskSummary,
|
||||
GroupPlanOverviewModal,
|
||||
toggleSwitch,
|
||||
},
|
||||
mixins: [sync],
|
||||
@@ -309,10 +306,6 @@ export default {
|
||||
if (!this.searchId) this.searchId = this.groupId;
|
||||
this.load();
|
||||
|
||||
if (this.$route.query.showGroupOverview) {
|
||||
this.$root.$emit('bv::show::modal', 'group-plan-overview');
|
||||
}
|
||||
|
||||
this.$root.$on('habitica:team-sync', () => {
|
||||
this.loadTasks();
|
||||
this.loadGroupCompletedTodos();
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
<template>
|
||||
<!-- @TODO: Move to group plans folder-->
|
||||
<div>
|
||||
<group-plan-creation-modal />
|
||||
<div>
|
||||
<div class="header">
|
||||
<h1
|
||||
v-once
|
||||
class="text-center"
|
||||
>
|
||||
{{ $t('groupPlanTitle') }}
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="col-8 offset-2 text-center">
|
||||
<h2
|
||||
v-once
|
||||
class="sub-text"
|
||||
>
|
||||
{{ $t('groupBenefitsDescription') }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container benefits">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="box">
|
||||
<img
|
||||
class="box1"
|
||||
src="~@/assets/images/group-plans/group-14@3x.png"
|
||||
>
|
||||
<hr>
|
||||
<h2 v-once>
|
||||
{{ $t('teamBasedTasks') }}
|
||||
</h2>
|
||||
<p v-once>
|
||||
{{ $t('teamBasedTasksListDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="box">
|
||||
<img
|
||||
class="box2"
|
||||
src="~@/assets/images/group-plans/group-12@3x.png"
|
||||
>
|
||||
<hr>
|
||||
<h2 v-once>
|
||||
{{ $t('groupManagementControls') }}
|
||||
</h2>
|
||||
<p v-once>
|
||||
{{ $t('groupManagementControlsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="box">
|
||||
<img
|
||||
class="box3"
|
||||
src="~@/assets/images/group-plans/group-13@3x.png"
|
||||
>
|
||||
<hr>
|
||||
<h2 v-once>
|
||||
{{ $t('inGameBenefits') }}
|
||||
</h2>
|
||||
<p v-once>
|
||||
{{ $t('inGameBenefitsDesc') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Upgrading an existing group -->
|
||||
<div
|
||||
v-if="upgradingGroup._id"
|
||||
id="upgrading-group"
|
||||
class="container payment-options"
|
||||
>
|
||||
<h1 class="text-center purple-header">
|
||||
Are you ready to upgrade?
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center mb-4 d-flex justify-content-center">
|
||||
<div class="purple-box">
|
||||
<div class="amount-section">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
9
|
||||
</div>
|
||||
<div class="name">
|
||||
Group Owner Subscription
|
||||
</div>
|
||||
</div>
|
||||
<div class="plus">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.positiveIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div class="amount-section">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
3
|
||||
</div>
|
||||
<div class="name">
|
||||
Each Individual Group Member
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box payment-providers">
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Create a new group -->
|
||||
<div
|
||||
v-if="!upgradingGroup._id"
|
||||
class="container col-6 offset-3 create-option"
|
||||
>
|
||||
<div class="row">
|
||||
<h1 class="col-12 text-center purple-header">
|
||||
Create Your Group Today!
|
||||
</h1>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary create-group"
|
||||
@click="launchModal('create-page')"
|
||||
>
|
||||
Create Your New Group!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pricing justify-content-center align-items-center">
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
9
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Group Owner</div>
|
||||
<div>Subscription</div>
|
||||
</div>
|
||||
<div class="plus">
|
||||
+
|
||||
</div>
|
||||
<div class="dollar">
|
||||
$
|
||||
</div>
|
||||
<div class="number">
|
||||
3
|
||||
</div>
|
||||
<div class="name">
|
||||
<div>Each Additional</div>
|
||||
<div>Member</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#upgrading-group {
|
||||
.amount-section {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dollar {
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
color: #bda8ff;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
}
|
||||
|
||||
.number {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.plus .svg-icon{
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.payment-providers {
|
||||
width: 350px;
|
||||
border-top-right-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #432874;
|
||||
background: linear-gradient(180deg, #4F2A93 0%, #432874 100%);
|
||||
color: #fff;
|
||||
padding: 32px;
|
||||
height: 340px;
|
||||
margin-bottom: 32px;
|
||||
margin-left: -12px;
|
||||
margin-right: -12px;
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
line-height: 1.16;
|
||||
margin-top: 12px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h2.sub-text {
|
||||
color: #D5C8FF;
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 1.33;
|
||||
}
|
||||
}
|
||||
|
||||
.benefits {
|
||||
margin-top: -10em;
|
||||
|
||||
.box {
|
||||
height: 416px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #6133b4;
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
display: inline-block !important;
|
||||
vertical-align: bottom;
|
||||
margin-right: 1em;
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
img.box1 {
|
||||
width: 266px;
|
||||
}
|
||||
|
||||
img.box2 {
|
||||
margin-top: 3.5em;
|
||||
width: 262px;
|
||||
margin-bottom: 3.7em;
|
||||
}
|
||||
|
||||
img.box3 {
|
||||
width: 225px;
|
||||
margin-bottom: 3.0em;
|
||||
}
|
||||
|
||||
button.create-group {
|
||||
width: 330px;
|
||||
height: 96px;
|
||||
border-radius: 8px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.purple-header {
|
||||
color: #6133b4;
|
||||
font-size: 48px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.pricing {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 64px;
|
||||
|
||||
.dollar, .number, .name {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.plus {
|
||||
font-size: 2.125rem;
|
||||
color: #a5a1ac;
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.dollar {
|
||||
margin-bottom: 24px;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.5rem;
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 4.5rem;
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-options {
|
||||
margin-bottom: 64px;
|
||||
|
||||
h4 {
|
||||
color: #34313a;
|
||||
}
|
||||
|
||||
.purple-box {
|
||||
background-color: #4f2a93;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
height: 215px;
|
||||
|
||||
.dollar {
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 100px;
|
||||
margin-left: 4.8px;
|
||||
}
|
||||
|
||||
.plus {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.box, .purple-box {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
import groupPlanCreationModal from '../group-plans/groupPlanCreationModal';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
paymentsButtons,
|
||||
groupPlanCreationModal,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
UPGRADE_GROUP: 'upgrade-group',
|
||||
PAY: 'pay',
|
||||
},
|
||||
PAYMENTS: {
|
||||
AMAZON: 'amazon',
|
||||
STRIPE: 'stripe',
|
||||
},
|
||||
paymentMethod: '',
|
||||
newGroup: {
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
name: '',
|
||||
leaderOnly: {
|
||||
challenges: false,
|
||||
},
|
||||
},
|
||||
activePage: '',
|
||||
type: 'guild', // Guild or Party @TODO enum this
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
newGroupIsReady () {
|
||||
return Boolean(this.newGroup.name);
|
||||
},
|
||||
upgradingGroup () {
|
||||
return this.$store.state.upgradingGroup;
|
||||
},
|
||||
// @TODO: can we move this to payment mixin?
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.activePage = this.PAGES.BENEFITS;
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('groupPlans'),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
launchModal () {
|
||||
this.$root.$emit('bv::show::modal', 'create-group');
|
||||
},
|
||||
createGroup () {
|
||||
this.changePage(this.PAGES.PAY);
|
||||
},
|
||||
pay (paymentMethod) {
|
||||
const subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
|
||||
const paymentData = {
|
||||
subscription: subscriptionKey,
|
||||
coupon: null,
|
||||
};
|
||||
|
||||
if (this.upgradingGroup && this.upgradingGroup._id) {
|
||||
paymentData.groupId = this.upgradingGroup._id;
|
||||
paymentData.group = this.upgradingGroup;
|
||||
} else {
|
||||
paymentData.groupToCreate = this.newGroup;
|
||||
}
|
||||
|
||||
this.paymentMethod = paymentMethod;
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.AMAZON) {
|
||||
paymentData.type = 'subscription';
|
||||
return paymentData;
|
||||
}
|
||||
|
||||
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
|
||||
this.redirectToStripe(paymentData);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -122,8 +122,8 @@
|
||||
<script>
|
||||
import clone from 'lodash/clone';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
class="brand"
|
||||
aria-label="Habitica"
|
||||
>
|
||||
<router-link to="/">
|
||||
<router-link to="/">
|
||||
<div
|
||||
class="logo svg-icon svg color gryphon"
|
||||
v-html="icons.melior"
|
||||
class="logo svg-icon svg color gryphon pl-2 mr-3"
|
||||
v-html="icons.melior"
|
||||
></div>
|
||||
<div class="svg-icon"></div>
|
||||
</router-link>
|
||||
@@ -349,15 +349,15 @@
|
||||
>
|
||||
<div
|
||||
v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')"
|
||||
class="top-menu-icon svg-icon"
|
||||
class="top-menu-icon svg-icon mr-1"
|
||||
v-html="icons.hourglasses"
|
||||
></div>
|
||||
<span>{{ userHourglasses }}</span>
|
||||
</div>
|
||||
<div class="item-with-icon">
|
||||
<div class="item-with-icon gem">
|
||||
<a
|
||||
v-b-tooltip.hover.bottom="$t('gems')"
|
||||
class="top-menu-icon svg-icon gem"
|
||||
class="top-menu-icon svg-icon gem mr-2"
|
||||
:aria-label="$t('gems')"
|
||||
href="#buy-gems"
|
||||
@click.prevent="showBuyGemsModal()"
|
||||
@@ -368,7 +368,7 @@
|
||||
<div class="item-with-icon gold">
|
||||
<div
|
||||
v-b-tooltip.hover.bottom="$t('gold')"
|
||||
class="top-menu-icon svg-icon"
|
||||
class="top-menu-icon svg-icon mr-2"
|
||||
:aria-label="$t('gold')"
|
||||
v-html="icons.gold"
|
||||
></div>
|
||||
@@ -409,6 +409,180 @@ body.modal-open #habitica-menu {
|
||||
@import '~@/assets/scss/utils.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
.menu-toggle {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#menu_collapse {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
z-index: 1080;
|
||||
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
|
||||
min-height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
a {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: $white;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.quick-menu {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.currency-tray {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
font-size: 16px;
|
||||
color: $white !important;
|
||||
font-weight: bold;
|
||||
transition: none;
|
||||
|
||||
.topbar-dropdown {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
>a {
|
||||
padding: .8em 1em !important;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: $white !important;
|
||||
background: $purple-200;
|
||||
|
||||
.topbar-dropdown {
|
||||
margin-top: 0; // Remove gap between navbar and drop-down.
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
display: list-item;
|
||||
|
||||
&.active {
|
||||
background: $purple-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $purple-300;
|
||||
text-decoration: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
color: $white;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.gem {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&.gold {
|
||||
margin-left: 12px;
|
||||
margin-right: 36px;
|
||||
}
|
||||
|
||||
&:focus ::v-deep .top-menu-icon.svg-icon,
|
||||
&:hover ::v-deep .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
& ::v-deep .top-menu-icon.svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
a.item-with-icon:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@keyframes rotateGemColors {
|
||||
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
|
||||
20% {
|
||||
fill: #46A7D9; /* Blue */
|
||||
}
|
||||
40% {
|
||||
fill: #925CF3; /* Purple */
|
||||
}
|
||||
60% {
|
||||
fill: #DE3F3F; /* Red */
|
||||
}
|
||||
80% {
|
||||
fill: #FA8537; /* Orange */
|
||||
}
|
||||
100% {
|
||||
fill: #FFB445; /* Yellow */
|
||||
}
|
||||
}
|
||||
|
||||
.gem:hover {
|
||||
cursor: pointer;
|
||||
|
||||
& ::v-deep path:nth-child(1) {
|
||||
animation: rotateGemColors 3s linear infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
background-color: $red-50;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -0.5em;
|
||||
padding: .2em;
|
||||
}
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.chevron {
|
||||
display: none
|
||||
@@ -416,12 +590,13 @@ body.modal-open #habitica-menu {
|
||||
|
||||
.gryphon {
|
||||
background-size: cover;
|
||||
height: 32px;
|
||||
color: $white;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
width: 32px;
|
||||
top: -10px;
|
||||
padding-left: 8px;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@@ -545,193 +720,23 @@ body.modal-open #habitica-menu {
|
||||
.desktop-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#menu_collapse {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
z-index: 1080;
|
||||
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
|
||||
min-height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
a {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: $white;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.quick-menu {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.currency-tray {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.topbar-item {
|
||||
font-size: 16px;
|
||||
color: $white !important;
|
||||
font-weight: bold;
|
||||
transition: none;
|
||||
|
||||
.topbar-dropdown {
|
||||
overflow: hidden;
|
||||
max-height: 0;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
line-height: 1.5;
|
||||
font-size: 16px;
|
||||
}
|
||||
.navbar-toggler {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
>a {
|
||||
padding: .8em 1em !important;
|
||||
}
|
||||
.item-with-icon {
|
||||
margin-left: 0px;
|
||||
margin-right: 16px;
|
||||
|
||||
&.down {
|
||||
color: $white !important;
|
||||
background: $purple-200;
|
||||
|
||||
.topbar-dropdown {
|
||||
margin-top: 0; // Remove gap between navbar and drop-down.
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.topbar-dropdown-item {
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
display: list-item;
|
||||
|
||||
&.active {
|
||||
background: $purple-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $purple-300;
|
||||
text-decoration: none;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
& ::v-deep .top-menu-icon.svg-icon {
|
||||
margin-right: 0px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
color: $white;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.gold {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:focus ::v-deep .top-menu-icon.svg-icon,
|
||||
&:hover ::v-deep .top-menu-icon.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
& ::v-deep .top-menu-icon.svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
a.item-with-icon:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
@keyframes rotateGemColors {
|
||||
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
|
||||
20% {
|
||||
fill: #46A7D9; /* Blue */
|
||||
}
|
||||
40% {
|
||||
fill: #925CF3; /* Purple */
|
||||
}
|
||||
60% {
|
||||
fill: #DE3F3F; /* Red */
|
||||
}
|
||||
80% {
|
||||
fill: #FA8537; /* Orange */
|
||||
}
|
||||
100% {
|
||||
fill: #FFB445; /* Yellow */
|
||||
}
|
||||
}
|
||||
|
||||
.gem:hover {
|
||||
cursor: pointer;
|
||||
|
||||
& ::v-deep path:nth-child(1) {
|
||||
animation: rotateGemColors 3s linear infinite alternate;
|
||||
}
|
||||
}
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
background-color: $red-50;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -0.5em;
|
||||
padding: .2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
.message-count {
|
||||
background-color: $red-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
height: 20px;
|
||||
left: 24px;
|
||||
text-align: center;
|
||||
width: 20px;
|
||||
|
||||
svg {
|
||||
width: 12px;
|
||||
@@ -36,4 +36,11 @@
|
||||
.message-count.top-count-gray {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
|
||||
.message-count {
|
||||
left: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:top="true"
|
||||
/>
|
||||
<div
|
||||
class="top-menu-icon svg-icon user"
|
||||
class="top-menu-icon svg-icon mr-2"
|
||||
v-html="icons.user"
|
||||
></div>
|
||||
</div>
|
||||
@@ -105,6 +105,11 @@
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@media only screen and (max-width: 992px) {
|
||||
.item-with-icon.item-user {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
<slot
|
||||
name="itemBadge"
|
||||
:item="item"
|
||||
></slot><span
|
||||
></slot><Sprite
|
||||
class="item-content"
|
||||
:class="itemContentClass"
|
||||
></span>
|
||||
:image-name="itemContentClass"
|
||||
/>
|
||||
</div><span
|
||||
v-if="label"
|
||||
class="item-label"
|
||||
@@ -46,8 +46,12 @@
|
||||
|
||||
<script>
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
|
||||
113
website/client/src/components/inventory/itemPopover.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div
|
||||
ref="root"
|
||||
v-if="draggedItem"
|
||||
class="draggedItemInfo mouse"
|
||||
v-mousePosition="30"
|
||||
@mouseMoved="mouseMoved($event)">
|
||||
<Sprite
|
||||
class="dragging-icon"
|
||||
:image-name="imageName()"
|
||||
/>
|
||||
<div class="popover">
|
||||
<div
|
||||
class="popover-content"
|
||||
>
|
||||
{{ $t(popoverTextKey, { [translationKey]: itemText() }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.draggedItemInfo {
|
||||
position: absolute;
|
||||
left: -500px;
|
||||
|
||||
z-index: 1080;
|
||||
|
||||
&.mouse {
|
||||
position: fixed;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.dragging-icon {
|
||||
width: 68px;
|
||||
margin: 0 auto 8px;
|
||||
display: block;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.popover {
|
||||
position: static;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
color: white;
|
||||
margin: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import MouseMoveDirective from '@/directives/mouseposition.directive';
|
||||
|
||||
export default {
|
||||
name: 'ItemPopover',
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
directives: {
|
||||
mousePosition: MouseMoveDirective,
|
||||
},
|
||||
props: {
|
||||
draggedItem: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
popoverTextKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
translationKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
imageName () {
|
||||
if (this.draggedItem) {
|
||||
if (this.draggedItem.class) {
|
||||
return this.draggedItem.class;
|
||||
}
|
||||
if (this.draggedItem.target) {
|
||||
return `Pet_Food_${this.draggedItem.key}`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
mouseMoved ($event) {
|
||||
if (this.$refs.root) {
|
||||
this.$refs.root.style.left = `${$event.x - 60}px`;
|
||||
this.$refs.root.style.top = `${$event.y + 10}px`;
|
||||
}
|
||||
},
|
||||
itemText () {
|
||||
if (this.draggedItem) {
|
||||
if (this.draggedItem.text) {
|
||||
if (typeof this.draggedItem.text === 'function') {
|
||||
return this.draggedItem.text();
|
||||
}
|
||||
return this.draggedItem.text;
|
||||
}
|
||||
return this.draggedItem.class;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-mousePosition="30"
|
||||
class="row"
|
||||
@mouseMoved="mouseMoved($event)"
|
||||
>
|
||||
<div class="standard-sidebar d-none d-sm-block">
|
||||
<filter-sidebar>
|
||||
@@ -99,7 +97,7 @@
|
||||
{{ context.item.text }}
|
||||
</h4>
|
||||
<div
|
||||
v-if="currentDraggingPotion == null"
|
||||
v-if="!currentDraggingPotion"
|
||||
class="popover-content-text"
|
||||
>
|
||||
{{ context.item.notes }}
|
||||
@@ -148,7 +146,7 @@
|
||||
<h4 class="popover-content-title">
|
||||
{{ context.item.text }}
|
||||
</h4>
|
||||
<div class="popover-content-text">
|
||||
<div class="popover-content-text" v-if="!currentDraggingEgg">
|
||||
{{ context.item.notes }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -224,120 +222,24 @@
|
||||
</div>
|
||||
</div>
|
||||
<hatchedPetDialog />
|
||||
<div
|
||||
ref="draggingEggInfo"
|
||||
class="eggInfo"
|
||||
>
|
||||
<div v-if="currentDraggingEgg != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="`Pet_Egg_${currentDraggingEgg.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div class="popover-content">
|
||||
{{ $t('dragThisEgg', {eggName: currentDraggingEgg.text }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="eggClickMode"
|
||||
ref="clickEggInfo"
|
||||
class="eggInfo mouse"
|
||||
>
|
||||
<div v-if="currentDraggingEgg != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="`Pet_Egg_${currentDraggingEgg.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
class="popover-content"
|
||||
>
|
||||
{{ $t('clickOnPotionToHatch', {eggName: currentDraggingEgg.text }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="draggingPotionInfo"
|
||||
class="hatchingPotionInfo"
|
||||
>
|
||||
<div v-if="currentDraggingPotion != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
class="popover-content"
|
||||
>
|
||||
{{ $t('dragThisPotion', {potionName: currentDraggingPotion.text }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="potionClickMode"
|
||||
ref="clickPotionInfo"
|
||||
class="hatchingPotionInfo mouse"
|
||||
>
|
||||
<div v-if="currentDraggingPotion != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
class="popover-content"
|
||||
>
|
||||
{{ $t('clickOnEggToHatch', {potionName: currentDraggingPotion.text }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ItemPopover
|
||||
:dragged-item="currentDraggingEgg"
|
||||
popoverTextKey="clickOnPotionToHatch"
|
||||
translationKey="eggName" />
|
||||
<ItemPopover
|
||||
:dragged-item="currentDraggingPotion"
|
||||
popoverTextKey="clickOnEggToHatch"
|
||||
translationKey="potionName" />
|
||||
<questDetailModal :group="user.party" />
|
||||
<cards-modal :card-options="cardOptions" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.eggInfo, .hatchingPotionInfo {
|
||||
position: absolute;
|
||||
left: -500px;
|
||||
|
||||
z-index: 1080;
|
||||
|
||||
&.mouse {
|
||||
position: fixed;
|
||||
pointer-events: none
|
||||
}
|
||||
|
||||
.potion-icon {
|
||||
margin: 0 auto 8px;
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.popover {
|
||||
position: inherit;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
color: white;
|
||||
margin: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import each from 'lodash/each';
|
||||
import throttle from 'lodash/throttle';
|
||||
import moment from 'moment';
|
||||
import ItemPopover from '@/components/inventory/itemPopover';
|
||||
import Item from '@/components/inventory/item';
|
||||
import ItemRows from '@/components/ui/itemRows';
|
||||
import CountBadge from '@/components/ui/countBadge';
|
||||
@@ -354,7 +256,6 @@ import { createAnimal } from '@/libs/createAnimal';
|
||||
|
||||
import notifications from '@/mixins/notifications';
|
||||
import DragDropDirective from '@/directives/dragdrop.directive';
|
||||
import MouseMoveDirective from '@/directives/mouseposition.directive';
|
||||
import FilterGroup from '@/components/ui/filterGroup';
|
||||
import Checkbox from '@/components/ui/checkbox';
|
||||
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
@@ -375,8 +276,6 @@ const groups = [
|
||||
allowedItems,
|
||||
}));
|
||||
|
||||
let lastMouseMoveEvent = {};
|
||||
|
||||
export default {
|
||||
name: 'Items',
|
||||
components: {
|
||||
@@ -391,10 +290,10 @@ export default {
|
||||
cardsModal,
|
||||
QuestInfo,
|
||||
FilterSidebar,
|
||||
ItemPopover,
|
||||
},
|
||||
directives: {
|
||||
drag: DragDropDirective,
|
||||
mousePosition: MouseMoveDirective,
|
||||
},
|
||||
mixins: [notifications],
|
||||
data () {
|
||||
@@ -405,9 +304,7 @@ export default {
|
||||
sortBy: 'quantity', // or 'AZ'
|
||||
|
||||
currentDraggingEgg: null,
|
||||
eggClickMode: false,
|
||||
currentDraggingPotion: null,
|
||||
potionClickMode: false,
|
||||
cardOptions: {
|
||||
cardType: '',
|
||||
messageOptions: 0,
|
||||
@@ -567,22 +464,13 @@ export default {
|
||||
}
|
||||
|
||||
this.currentDraggingPotion = null;
|
||||
this.potionClickMode = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentDraggingEgg === null || this.currentDraggingEgg !== egg) {
|
||||
this.currentDraggingEgg = egg;
|
||||
this.eggClickMode = true;
|
||||
|
||||
// Wait for the div.eggInfo.mouse node to be added to the DOM before
|
||||
// changing its position.
|
||||
this.$nextTick(() => {
|
||||
this.mouseMoved(lastMouseMoveEvent);
|
||||
});
|
||||
} else {
|
||||
this.currentDraggingEgg = null;
|
||||
this.eggClickMode = false;
|
||||
}
|
||||
},
|
||||
onPotionClicked ($event, potion) {
|
||||
@@ -592,21 +480,12 @@ export default {
|
||||
}
|
||||
|
||||
this.currentDraggingEgg = null;
|
||||
this.eggClickMode = false;
|
||||
return;
|
||||
}
|
||||
if (this.currentDraggingPotion === null || this.currentDraggingPotion !== potion) {
|
||||
this.currentDraggingPotion = potion;
|
||||
this.potionClickMode = true;
|
||||
|
||||
// Wait for the div.hatchingPotionInfo.mouse node to be added to the
|
||||
// DOM before changing its position.
|
||||
this.$nextTick(() => {
|
||||
this.mouseMoved(lastMouseMoveEvent);
|
||||
});
|
||||
} else {
|
||||
this.currentDraggingPotion = null;
|
||||
this.potionClickMode = false;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -640,23 +519,6 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mouseMoved ($event) {
|
||||
// Keep track of the last mouse position even in click mode so that we
|
||||
// know where to position the dragged potion/egg info on item click.
|
||||
lastMouseMoveEvent = $event;
|
||||
|
||||
// Update the potion/egg popover if we are already dragging it.
|
||||
if (this.potionClickMode) {
|
||||
// dragging potioninfo is 180px wide (90 would be centered)
|
||||
this.$refs.clickPotionInfo.style.left = `${$event.x - 60}px`;
|
||||
this.$refs.clickPotionInfo.style.top = `${$event.y + 10}px`;
|
||||
} else if (this.eggClickMode) {
|
||||
// dragging eggInfo is 180px wide (90 would be centered)
|
||||
this.$refs.clickEggInfo.style.left = `${$event.x - 60}px`;
|
||||
this.$refs.clickEggInfo.style.top = `${$event.y + 10}px`;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
:show="true"
|
||||
:count="itemCount"
|
||||
/>
|
||||
<span
|
||||
<Sprite
|
||||
v-drag.food="item.key"
|
||||
class="item-content"
|
||||
:class="`Pet_Food_${item.key}`"
|
||||
:image-name="`Pet_Food_${item.key}`"
|
||||
@itemDragEnd="dragend($event)"
|
||||
@itemDragStart="dragstart($event)"
|
||||
></span>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<b-popover
|
||||
@@ -41,12 +41,14 @@
|
||||
<script>
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import DragDropDirective from '@/directives/dragdrop.directive';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
import CountBadge from '@/components/ui/countBadge';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CountBadge,
|
||||
Sprite,
|
||||
},
|
||||
directives: {
|
||||
drag: DragDropDirective,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="inner-content">
|
||||
<div class="pet-background d-flex align-items-center">
|
||||
<div :class="pet.class"></div>
|
||||
<Sprite :image-name="pet.imageName" />
|
||||
</div>
|
||||
<h4 class="title">
|
||||
{{ pet.name }}
|
||||
@@ -76,10 +76,11 @@
|
||||
height: 112px;
|
||||
border-radius: 4px;
|
||||
background-color: $gray-700;
|
||||
}
|
||||
|
||||
.Pet {
|
||||
margin: auto;
|
||||
img {
|
||||
transform: scale(1.5);
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
@@ -103,8 +104,12 @@
|
||||
<script>
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sprite,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
|
||||