Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af17930314 | |||
| 094b19f289 | |||
| 8e54cef68b | |||
| 1df8d5832f | |||
| 0542008b7f | |||
| ffa89202e6 | |||
| 1203cbbad8 | |||
| f9fb463128 | |||
| 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 | |||
| 3f3e2525d2 |
@@ -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: |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -22,7 +22,8 @@ services:
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
- mongo
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
networks:
|
||||
@@ -33,7 +34,16 @@ services:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
image: mongo:5.0.23
|
||||
restart: unless-stopped
|
||||
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.27.0",
|
||||
"version": "5.28.8",
|
||||
"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",
|
||||
@@ -75,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"
|
||||
@@ -105,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),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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-20'));
|
||||
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-20'));
|
||||
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') {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
each,
|
||||
} from 'lodash';
|
||||
import {
|
||||
expectValidTranslationString,
|
||||
} from '../helpers/content.helper';
|
||||
|
||||
import { quests } from '../../website/common/script/content/quests';
|
||||
|
||||
describe('quests', () => {
|
||||
let clock;
|
||||
|
||||
afterEach(() => {
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
});
|
||||
|
||||
it('contains basic information about each quest', () => {
|
||||
each(quests, (quest, key) => {
|
||||
expectValidTranslationString(quest.text);
|
||||
expectValidTranslationString(quest.notes);
|
||||
expectValidTranslationString(quest.completion);
|
||||
expect(quest.key, key).to.equal(key);
|
||||
expect(quest.category, key).to.be.a('string');
|
||||
if (quest.boss) {
|
||||
expectValidTranslationString(quest.boss.name);
|
||||
expect(quest.boss.hp, key).to.be.a('number');
|
||||
expect(quest.boss.str, key).to.be.a('number');
|
||||
}
|
||||
expect(quest.drop).to.be.an('object');
|
||||
expect(quest.drop.gp, key).to.be.a('number');
|
||||
expect(quest.drop.exp, key).to.be.a('number');
|
||||
if (quest.drop.items) {
|
||||
quest.drop.items.forEach(drop => {
|
||||
expectValidTranslationString(drop.text);
|
||||
expect(drop.type, key).to.exist;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
@@ -129,6 +142,42 @@ describe('Content Schedule', () => {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
@@ -39,7 +38,6 @@
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"smartbanner.js": "^1.19.3",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -59,7 +57,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 +2305,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 +3118,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 +3137,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 +3157,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 +3189,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 +3216,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 +3240,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 +3315,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 +4035,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 +5387,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 +5480,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 +6855,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 +8992,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": {
|
||||
@@ -12095,17 +12079,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/smartbanner.js": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/smartbanner.js/-/smartbanner.js-1.22.0.tgz",
|
||||
"integrity": "sha512-JhERLgwEPuzVdwAHds1J6txWBVq9BwmlAn+5VicrAfIOMO3ehNA7VHu8IIJNnW1LsElSCaLWxjdLjlEwLDqAvA==",
|
||||
"engines": {
|
||||
"node": ">=10.24.1 <22.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ain"
|
||||
}
|
||||
},
|
||||
"node_modules/sockjs": {
|
||||
"version": "0.3.24",
|
||||
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
|
||||
@@ -13342,9 +13315,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 +13351,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",
|
||||
@@ -41,7 +40,6 @@
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^14.1.1",
|
||||
"sinon": "^17.0.1",
|
||||
"smartbanner.js": "^1.19.3",
|
||||
"stopword": "^2.0.8",
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
@@ -61,6 +59,6 @@
|
||||
"chai": "^5.1.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"webpack": "^5.89.0"
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,6 @@
|
||||
<title>Habitica - Gamify Your Life</title>
|
||||
<meta name="description" content="Habitica is a free habit and productivity app that treats your real life like a game. Habitica can help you achieve your goals to become healthy and happy.">
|
||||
<meta name="keywords" content="Habits,Goals,Todo,Gamification,Health,Fitness,School,Work">
|
||||
<meta name="smartbanner:title" content="Habitica">
|
||||
<meta name="smartbanner:author" content="HabitRPG, Inc.">
|
||||
<meta name="smartbanner:price" content="FREE">
|
||||
<meta name="smartbanner:price-suffix-apple" content=" - On the App Store">
|
||||
<meta name="smartbanner:price-suffix-google" content=" - In Google Play">
|
||||
<meta name="smartbanner:icon-apple" content="/static/presskit/Logo/iOS.png">
|
||||
<meta name="smartbanner:icon-google" content="/static/presskit/Logo/Android.png">
|
||||
<meta name="smartbanner:button" content="VIEW">
|
||||
<meta name="smartbanner:button-url-apple" content="https://itunes.apple.com/us/app/habitica-gamified-taskmanager/id994882113">
|
||||
<meta name="smartbanner:button-url-google" content="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica">
|
||||
<meta name="smartbanner:enabled-platforms" content="android,ios">
|
||||
<meta name="smartbanner:hide-ttl" content="2592000000">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
|
||||
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
|
||||
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
|
||||
|
||||
@@ -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,9 @@ 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>
|
||||
|
||||
@@ -1580,6 +1580,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_magic_door_in_forest {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_magic_door_in_forest.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_magical_candles {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_magical_candles.png');
|
||||
width: 141px;
|
||||
@@ -2160,6 +2165,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_surrounded_by_ghosts {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_surrounded_by_ghosts.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_swan_boat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_swan_boat.png');
|
||||
width: 141px;
|
||||
@@ -29619,6 +29629,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_funnyFoolCostume {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_funnyFoolCostume.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_gardenersOveralls {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_gardenersOveralls.png');
|
||||
width: 114px;
|
||||
@@ -30189,6 +30204,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_funnyFoolCap {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_funnyFoolCap.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_gardenersSunHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_gardenersSunHat.png');
|
||||
width: 114px;
|
||||
@@ -30779,6 +30799,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_safetyFlashlight {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_safetyFlashlight.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_sandyBucket {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_sandyBucket.png');
|
||||
width: 90px;
|
||||
@@ -31069,6 +31094,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_funnyFoolCostume {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_funnyFoolCostume.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_gardenersOveralls {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_gardenersOveralls.png');
|
||||
width: 114px;
|
||||
@@ -31584,6 +31614,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_funnyFoolBaton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_funnyFoolBaton.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gardenersWateringCan {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png');
|
||||
width: 114px;
|
||||
@@ -31909,6 +31944,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_spookyCandyBucket {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_spookyCandyBucket.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_vermilionArcherBow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_vermilionArcherBow.png');
|
||||
width: 90px;
|
||||
@@ -32724,6 +32764,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2024Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2024Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2024Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2024Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2024Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2024Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2024Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2024Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -32934,6 +32994,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2024Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2024Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2024Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2024Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2024Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2024Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2024Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2024Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33089,6 +33169,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2024Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2024Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2024Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2024Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2024Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2024Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33284,6 +33379,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2024Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2024Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2024Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2024Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2024Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2024Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2024Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2024Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33484,6 +33599,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2024Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2024Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2024Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2024Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2024Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2024Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2024Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2024Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -35149,6 +35284,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202409 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202409.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202409 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202409.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202410 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202410.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.headAccessory_mystery_202410 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_mystery_202410.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
@@ -40107,6 +40262,11 @@
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dog {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_dog.png');
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_dolphin {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_dolphin.png');
|
||||
width: 219px;
|
||||
@@ -40327,6 +40487,11 @@
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_raccoon {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_raccoon.png');
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
.quest_rat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_rat.png');
|
||||
width: 219px;
|
||||
@@ -40832,6 +40997,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dog {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_dog.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_dolphin {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_dolphin.png');
|
||||
width: 68px;
|
||||
@@ -41132,6 +41302,11 @@
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_raccoon {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_raccoon.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.inventory_quest_scroll_rat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/inventory_quest_scroll_rat.png');
|
||||
width: 68px;
|
||||
@@ -42667,6 +42842,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dog-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dog-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Dolphin-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dolphin-Base.png');
|
||||
width: 105px;
|
||||
@@ -45157,6 +45382,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Raccoon-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Raccoon-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Body_Rat-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Rat-Base.png');
|
||||
width: 105px;
|
||||
@@ -48062,6 +48337,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dog-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dog-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Dolphin-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dolphin-Base.png');
|
||||
width: 105px;
|
||||
@@ -50552,6 +50877,56 @@
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Base.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-CottonCandyBlue.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-CottonCandyPink.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Desert.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Golden.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Red.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Shade.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Skeleton.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-White.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Raccoon-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Raccoon-Zombie.png');
|
||||
width: 105px;
|
||||
height: 105px;
|
||||
}
|
||||
.Mount_Head_Rat-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Rat-Base.png');
|
||||
width: 105px;
|
||||
@@ -53517,6 +53892,56 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Base.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-CottonCandyBlue.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-CottonCandyPink.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Desert.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Golden.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Red.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Shade.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Skeleton.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-White.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dog-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dog-Zombie.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dolphin-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dolphin-Base.png');
|
||||
width: 81px;
|
||||
@@ -56147,6 +56572,56 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Base.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-CottonCandyBlue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-CottonCandyBlue.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-CottonCandyPink {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-CottonCandyPink.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Desert {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Desert.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Golden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Golden.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Red {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Red.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Shade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Shade.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Skeleton {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Skeleton.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-White {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-White.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Raccoon-Zombie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Raccoon-Zombie.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Rat-Base {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Rat-Base.png');
|
||||
width: 81px;
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 26 KiB |
|
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;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
@hide="hide"
|
||||
>
|
||||
<div class="modal-body text-center">
|
||||
<div
|
||||
<Sprite
|
||||
class="quest"
|
||||
:class="`quest_${user.party.quest.completed}`"
|
||||
></div>
|
||||
:image-name="`quest_${user.party.quest.completed}`"
|
||||
/>
|
||||
<p
|
||||
v-if="questData.completion && typeof questData.completion === 'function'"
|
||||
v-html="questData.completion()"
|
||||
@@ -58,10 +58,12 @@ import percent from '@/../../common/script/libs/percent';
|
||||
import { MAX_HEALTH as maxHealth } from '@/../../common/script/constants';
|
||||
import { mapState } from '@/libs/store';
|
||||
import QuestRewards from '../shops/quests/questRewards';
|
||||
import Sprite from '../ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QuestRewards,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="pull-right-sm text-center">
|
||||
<div
|
||||
class="col-centered"
|
||||
:class="`quest_${quests[user.party.quest.key].key}`"
|
||||
></div>
|
||||
<div class="col-centered">
|
||||
<Sprite
|
||||
:image-name="`quest_${quests[user.party.quest.key].key}`"
|
||||
/>
|
||||
</div>
|
||||
<div ng-if="quests[user.party.quest.key].boss">
|
||||
<h4>{{ quests[user.party.quest.key].boss.name() }}</h4>
|
||||
<p>
|
||||
@@ -93,8 +94,12 @@ import * as quests from '@/../../common/script/content/quests';
|
||||
import percent from '@/../../common/script/libs/percent';
|
||||
import { MAX_HEALTH as maxHealth } from '@/../../common/script/constants';
|
||||
import { mapState } from '@/libs/store';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
|
||||
export default {
|
||||
components: [
|
||||
Sprite,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
maxHealth,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -51,10 +51,10 @@
|
||||
:class="{'not-participating': !userIsOnQuest}"
|
||||
>
|
||||
<div class="col-12 text-center">
|
||||
<div
|
||||
<Sprite
|
||||
class="quest-boss"
|
||||
:class="'quest_' + questData.key"
|
||||
></div>
|
||||
:image-name="'quest_' + questData.key"
|
||||
/>
|
||||
<div class="quest-box">
|
||||
<div
|
||||
v-if="questData.collect"
|
||||
@@ -66,7 +66,7 @@
|
||||
class="quest-item-row"
|
||||
>
|
||||
<div class="quest-item-icon">
|
||||
<div :class="'quest_' + questData.key + '_' + key"></div>
|
||||
<Sprite :image-name="'quest_' + questData.key + '_' + key" />
|
||||
</div>
|
||||
<div class="quest-item-info">
|
||||
<span class="label quest-label">{{ value.text() }}</span>
|
||||
@@ -643,6 +643,7 @@ import * as quests from '@/../../common/script/content/quests';
|
||||
import percent from '@/../../common/script/libs/percent';
|
||||
import { mapState } from '@/libs/store';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import Sprite from '../ui/sprite';
|
||||
|
||||
import questIcon from '@/assets/svg/quest.svg';
|
||||
import swordIcon from '@/assets/svg/sword.svg';
|
||||
@@ -653,6 +654,7 @@ import questActionsMixin from '@/components/groups/questActions.mixin';
|
||||
export default {
|
||||
components: {
|
||||
sidebarSection,
|
||||
Sprite,
|
||||
},
|
||||
mixins: [questActionsMixin],
|
||||
props: ['group'],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -529,7 +529,7 @@ export default {
|
||||
|
||||
// List of prompts for user on changes.
|
||||
// Sounds like we may need a refactor here, but it is clean for now
|
||||
if (!this.user.flags.welcomed) {
|
||||
if (!this.user.flags.welcomed && !this.$route.name.includes('groupPlan')) {
|
||||
if (this.$store.state.avatarEditorOptions) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = false;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,6 @@
|
||||
:alt="$t('paypal')"
|
||||
>
|
||||
</button>
|
||||
<amazon-button
|
||||
v-if="amazonAvailable"
|
||||
class="payment-item"
|
||||
:disabled="disabled"
|
||||
:amazon-data="amazonData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -92,21 +86,14 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import amazonButton from '@/components/payments/buttons/amazon';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
amazonData: {
|
||||
type: Object,
|
||||
},
|
||||
stripeFn: {
|
||||
type: Function,
|
||||
},
|
||||
@@ -128,9 +115,6 @@ export default {
|
||||
paypalAvailable () {
|
||||
return typeof this.paypalFn === 'function';
|
||||
},
|
||||
amazonAvailable () {
|
||||
return this.amazonData !== undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
id="buy-gems"
|
||||
:hide-footer="true"
|
||||
size="md"
|
||||
:modal-class="eventClass"
|
||||
:modal-class="eventInfo?.class"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
@@ -21,7 +21,7 @@
|
||||
class="col-12 text-center"
|
||||
>
|
||||
<img
|
||||
v-if="eventName === 'fall_extra_gems'"
|
||||
v-if="eventInfo?.name === 'fall_extra_gems'"
|
||||
:alt="$t('supportHabitica')"
|
||||
srcset="
|
||||
~@/assets/images/gems/fall-header.png,
|
||||
@@ -30,7 +30,7 @@
|
||||
src="~@/assets/images/gems/fall-header.png"
|
||||
>
|
||||
<img
|
||||
v-else-if="eventName === 'spooky_extra_gems'"
|
||||
v-else-if="eventInfo?.name === 'spooky_extra_gems'"
|
||||
:alt="$t('supportHabitica')"
|
||||
srcset="
|
||||
~@/assets/images/gems/spooky-header.png,
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentEvent && currentEvent.promo && currentEvent.promo === 'g1g1'"
|
||||
v-if="eventInfo?.promo === 'g1g1'"
|
||||
class="gift-promo-banner d-flex justify-content-around align-items-center px-4"
|
||||
@click="showSelectUser"
|
||||
>
|
||||
@@ -162,24 +162,31 @@
|
||||
:amazon-data="{type: 'single', gemsBlock: selectedGemsBlock}"
|
||||
/>
|
||||
<div
|
||||
v-if="eventName === 'fall_extra_gems' || eventName === 'spooky_extra_gems'"
|
||||
v-if="eventInfo?.name === 'fall_extra_gems' || eventInfo?.name === 'spooky_extra_gems'"
|
||||
class="d-flex flex-column justify-content-center"
|
||||
>
|
||||
<h4 class="mt-3 mx-auto">
|
||||
{{ $t('howItWorks') }}
|
||||
</h4>
|
||||
<small class="text-center">
|
||||
{{ $t('gemSaleHow', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
|
||||
{{ $t('gemSaleHow', {
|
||||
eventStartMonth: eventInfo.startMonth,
|
||||
eventStartOrdinal: eventInfo.startOrdinal,
|
||||
eventEndOrdinal: eventInfo.endOrdinal,
|
||||
}) }}
|
||||
</small>
|
||||
<h4 class="mt-3 mx-auto">
|
||||
{{ $t('limitations') }}
|
||||
</h4>
|
||||
<small class="text-center">
|
||||
{{ $t('gemSaleLimitations', {
|
||||
eventStartMonth,
|
||||
eventStartOrdinal,
|
||||
eventEndMonth,
|
||||
eventEndOrdinal,
|
||||
{{ $t('gemSaleLimitationsText', {
|
||||
eventStartMonth: eventInfo.startMonth,
|
||||
eventStartOrdinal: eventInfo.startOrdinal,
|
||||
eventStartTime: eventInfo.startTime,
|
||||
eventEndMonth: eventInfo.endMonth,
|
||||
eventEndOrdinal: eventInfo.endOrdinal,
|
||||
eventEndTime: eventInfo.endTime,
|
||||
timeZone: eventInfo.timeZoneAbbrev,
|
||||
}) }}
|
||||
</small>
|
||||
</div>
|
||||
@@ -431,37 +438,34 @@ export default {
|
||||
originalGemsBlocks: 'content.gems',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
currentEvent () {
|
||||
return find(this.currentEventList, event => Boolean(event.gemsPromo) || Boolean(event.promo));
|
||||
},
|
||||
eventName () {
|
||||
return this.currentEvent && this.currentEvent.event;
|
||||
},
|
||||
eventClass () {
|
||||
if (this.currentEvent && this.currentEvent.gemsPromo) {
|
||||
return `event-${this.eventName}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
eventStartMonth () {
|
||||
return moment(this.currentEvent.start).format('MMMM');
|
||||
},
|
||||
eventStartOrdinal () {
|
||||
return moment(this.currentEvent.start).format('Do');
|
||||
},
|
||||
eventEndMonth () {
|
||||
return moment(this.currentEvent.end).format('MMMM');
|
||||
},
|
||||
eventEndOrdinal () {
|
||||
return moment(this.currentEvent.end).format('Do');
|
||||
eventInfo () {
|
||||
const currentEvent = find(
|
||||
this.currentEventList, event => Boolean(event.gemsPromo) || Boolean(event.promo),
|
||||
);
|
||||
if (!currentEvent) return null;
|
||||
|
||||
// https://stackoverflow.com/questions/1954397/detect-timezone-abbreviation-using-javascript#answer-66180857
|
||||
const timeZoneAbbrev = new Intl.DateTimeFormat('en-us', { timeZoneName: 'short' })
|
||||
.formatToParts(new Date())
|
||||
.find(part => part.type === 'timeZoneName')
|
||||
.value;
|
||||
|
||||
return {
|
||||
name: currentEvent.event,
|
||||
class: currentEvent.gemsPromo ? `event-${currentEvent.event}` : '',
|
||||
gemsPromo: currentEvent.gemsPromo,
|
||||
promo: currentEvent.promo,
|
||||
timeZoneAbbrev,
|
||||
startMonth: moment(currentEvent.start).format('MMMM'),
|
||||
startOrdinal: moment(currentEvent.start).format('Do'),
|
||||
startTime: moment(currentEvent.start).format('hh:mm A'),
|
||||
endMonth: moment(currentEvent.end).format('MMMM'),
|
||||
endOrdinal: moment(currentEvent.end).format('Do'),
|
||||
endTime: moment(currentEvent.end).format('hh:mm A'),
|
||||
};
|
||||
},
|
||||
isGemsPromoActive () {
|
||||
const currEvt = this.currentEvent;
|
||||
if (currEvt && currEvt.gemsPromo && moment().isBefore(currEvt.end)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return Boolean(this.eventInfo);
|
||||
},
|
||||
gemsBlocks () {
|
||||
// We don't want to modify the original gems blocks when a promotion is running
|
||||
@@ -476,7 +480,7 @@ export default {
|
||||
if (this.isGemsPromoActive) {
|
||||
newBlock.originalGems = originalBlock.gems;
|
||||
newBlock.gems = (
|
||||
this.currentEvent.gemsPromo[gemsBlockKey] || originalBlock.gems
|
||||
this.eventInfo.gemsPromo[gemsBlockKey] || originalBlock.gems
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -295,7 +295,7 @@ h2 {
|
||||
// import { nextTick } from 'vue'; // may not need this? I don't know!
|
||||
import debounce from 'lodash/debounce';
|
||||
import find from 'lodash/find';
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import isUUID from 'validator/es/lib/isUUID';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
|
||||
@@ -534,7 +534,7 @@
|
||||
color: $white;
|
||||
height: 2rem;
|
||||
line-height: 16px;
|
||||
margin: auto -1rem -1rem;
|
||||
margin: 24px auto -24px;
|
||||
}
|
||||
|
||||
.gems-left {
|
||||
@@ -847,7 +847,7 @@ export default {
|
||||
}
|
||||
if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
this.purchased(this.item.text);
|
||||
await this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,9 +39,16 @@ export const QuestHelperMixin = {
|
||||
return !drop.onlyOwner;
|
||||
}).map(item => {
|
||||
if (item.type === 'gear') {
|
||||
const contentItem = this.content.gear.flat[item.key];
|
||||
return this.content.gear.flat[item.key];
|
||||
}
|
||||
|
||||
return contentItem;
|
||||
if (item.type === 'quests') {
|
||||
const questScroll = {};
|
||||
Object.assign(questScroll, this.content.quests[item.key]);
|
||||
questScroll.type = 'quests';
|
||||
questScroll.text = item.text();
|
||||
questScroll.onlyOwner = item.onlyOwner;
|
||||
return questScroll;
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="quest-content">
|
||||
<div
|
||||
<Sprite
|
||||
class="quest-image"
|
||||
:class="item.purchaseType === 'bundles' ? `quest_bundle_${item.key}` : `quest_${item.key}`"
|
||||
></div>
|
||||
:image-name="item.purchaseType === 'bundles'
|
||||
? `quest_bundle_${item.key}` : `quest_${item.key}`"
|
||||
/>
|
||||
<h3 class="text-center">
|
||||
{{ itemText }}
|
||||
</h3>
|
||||
@@ -40,6 +41,7 @@
|
||||
margin: 0 auto;
|
||||
margin-bottom: 16px;
|
||||
margin-top: 24px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.leader-label {
|
||||
@@ -67,11 +69,13 @@
|
||||
<script>
|
||||
import QuestInfo from './questInfo.vue';
|
||||
import UserLabel from '../../userLabel';
|
||||
import Sprite from '../../ui/sprite';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
UserLabel,
|
||||
QuestInfo,
|
||||
Sprite,
|
||||
},
|
||||
props: {
|
||||
item: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="notifications"
|
||||
:class="notificationsTopPosClass"
|
||||
:style="{'--current-scrollY': notificationTopY}"
|
||||
>
|
||||
<transition-group
|
||||
@@ -104,7 +103,6 @@ export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
notificationStore: 'notificationStore',
|
||||
userSleeping: 'user.data.preferences.sleep',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
currentEvent () {
|
||||
@@ -113,18 +111,6 @@ export default {
|
||||
isEventActive () {
|
||||
return Boolean(this.currentEvent?.event);
|
||||
},
|
||||
notificationsTopPosClass () {
|
||||
const base = 'notifications-top-pos-';
|
||||
let modifier = '';
|
||||
|
||||
if (this.userSleeping) {
|
||||
modifier = 'sleeping';
|
||||
} else {
|
||||
modifier = 'normal';
|
||||
}
|
||||
|
||||
return `${base}${modifier} scroll-${this.scrollY}`;
|
||||
},
|
||||
notificationBannerHeight () {
|
||||
let scrollPosToCheck = 56;
|
||||
|
||||
|
||||
@@ -1,228 +1,204 @@
|
||||
<template>
|
||||
<div class="group-plan-static text-center">
|
||||
<amazon-payments-modal />
|
||||
<div class="container">
|
||||
<div class="row top">
|
||||
<div>
|
||||
<group-plan-creation-modal />
|
||||
<div class="d-flex justify-content-center">
|
||||
<div
|
||||
class="group-plan-page text-center"
|
||||
:class="{ static: isStaticPage }"
|
||||
>
|
||||
<div class="top-left"></div>
|
||||
<div class="col-6 offset-3">
|
||||
<div class="col-6 offset-3 mb-100">
|
||||
<img
|
||||
class="party"
|
||||
src="../../assets/images/group-plans-static/party@3x.png"
|
||||
>
|
||||
<h1>{{ $t('groupPlanTitle') }}</h1>
|
||||
<p>{{ $t('groupPlanDesc') }}</p>
|
||||
<div class="pricing">
|
||||
<h1 class="mt-5" v-if="upgradingGroup._id">{{ $t('upgradeYourCrew') }}</h1>
|
||||
<h1 class="mt-5" v-else>{{ $t('groupPlanTitle') }}</h1>
|
||||
<p class="mb-0">{{ $t('groupPlanDesc') }}</p>
|
||||
<div class="pricing mt-5">
|
||||
<span>Just</span>
|
||||
<span class="number">$9</span>
|
||||
<span class="bold">per month +</span>
|
||||
<span class="number">$3</span>
|
||||
<span class="bold">per member*</span>
|
||||
<span class="bold">per additional member*</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="btn btn-primary cta-button"
|
||||
class="btn btn-primary cta-button white mt-4 mb-3"
|
||||
@click="goToNewGroupPage()"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</div>
|
||||
<small>{{ $t('billedMonthly') }}</small>
|
||||
<p class="gray-200">{{ $t('billedMonthly') }}</p>
|
||||
</div>
|
||||
<div class="top-right"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="text-col col-12 col-md-6 text-left">
|
||||
<h2>{{ $t('teamBasedTasksList') }}</h2>
|
||||
<p>{{ $t('teamBasedTasksListDesc') }}</p>
|
||||
<div class="d-flex justify-content-between align-items-middle w-100 gap-72 mb-100">
|
||||
<div class="ml-auto my-auto w-448 text-left">
|
||||
<h2 class="mt-0">{{ $t('teamBasedTasksList') }}</h2>
|
||||
<p>{{ $t('teamBasedTasksListDesc') }}</p>
|
||||
</div>
|
||||
<div class="mr-auto my-auto">
|
||||
<img src="../../assets/images/group-plans-static/group-management@3x.png">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div
|
||||
class="team-based"
|
||||
v-html="svg.teamBased"
|
||||
></div>
|
||||
<div class="d-flex justify-content-between align-items-middle w-100 gap-72 mb-100">
|
||||
<div class="ml-auto my-auto">
|
||||
<img src="../../assets/images/group-plans-static/team-based@3x.png">
|
||||
</div>
|
||||
<div class="mr-auto my-auto w-448 text-left">
|
||||
<h2 class="mt-0">{{ $t('groupManagementControls') }}</h2>
|
||||
<p>{{ $t('groupManagementControlsDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div
|
||||
class="group-management"
|
||||
v-html="svg.groupManagement"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-col col-12 col-md-6 text-left">
|
||||
<h2>{{ $t('groupManagementControls') }}</h2>
|
||||
<p>{{ $t('groupManagementControlsDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 offset-md-3 text-center">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<img
|
||||
class="big-gem"
|
||||
class="big-gem mb-3 mx-auto"
|
||||
src="../../assets/images/group-plans-static/big-gem@3x.png"
|
||||
>
|
||||
<h2>{{ $t('inGameBenefits') }}</h2>
|
||||
<p>{{ $t('inGameBenefitsDesc') }}</p>
|
||||
<h2 class="mt-3">{{ $t('inGameBenefits') }}</h2>
|
||||
<p class="final-paragraph mx-auto">{{ $t('inGameBenefitsDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="bot-left"></div>
|
||||
<div class="col-6 offset-3">
|
||||
<h2 class="purple">
|
||||
{{ $t('inspireYourParty') }}
|
||||
</h2>
|
||||
<div class="pricing">
|
||||
<span>Just</span>
|
||||
<span class="number">$9</span>
|
||||
<span class="bold">per month +</span>
|
||||
<span class="number">$3</span>
|
||||
<span class="bold">per member*</span>
|
||||
<div class="text-center mb-128">
|
||||
<div class="bot-left"></div>
|
||||
<div class="col-6 offset-3">
|
||||
<h2 class="purple-300 mt-0 mb-4" v-if="upgradingGroup._id">
|
||||
{{ $t('readyToUpgrade') }}
|
||||
</h2>
|
||||
<h2 v-else class="purple-300 mt-0 mb-4">
|
||||
{{ $t('createGroupToday') }}
|
||||
</h2>
|
||||
<div class="pricing mb-4">
|
||||
<span>Just</span>
|
||||
<span class="number">$9</span>
|
||||
<span class="bold">per month +</span>
|
||||
<span class="number">$3</span>
|
||||
<span class="bold">per member*</span>
|
||||
</div>
|
||||
<div class="text-center mb-3">
|
||||
<button
|
||||
class="btn btn-primary cta-button white"
|
||||
@click="goToNewGroupPage()"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="gray-200">{{ $t('billedMonthly') }}</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="btn btn-primary cta-button"
|
||||
@click="goToNewGroupPage()"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</div>
|
||||
<small>{{ $t('billedMonthly') }}</small>
|
||||
<div class="bot-right"></div>
|
||||
</div>
|
||||
<div class="bot-right"></div>
|
||||
<b-modal
|
||||
id="group-plan"
|
||||
title
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ $t('letsMakeAccount') }}</h2>
|
||||
<auth-form @authenticate="authenticate()" />
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal
|
||||
id="group-plan"
|
||||
title
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
<div
|
||||
class="bottom-banner text-center"
|
||||
:class="{ static: isStaticPage }"
|
||||
>
|
||||
<div v-if="modalPage === 'account'">
|
||||
<h2>{{ $t('letsMakeAccount') }}</h2>
|
||||
<auth-form @authenticate="authenticate()" />
|
||||
</div>
|
||||
<div v-if="modalPage === 'purchaseGroup'">
|
||||
<create-group-modal-pages />
|
||||
</div>
|
||||
</b-modal>
|
||||
<h2 class="white">{{ $t('interestedLearningMore') }}</h2>
|
||||
<p class="purple-600" v-html="$t('checkGroupPlanFAQ')"></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
.bottom-banner > .purple-600 {
|
||||
color: #D5C8FF !important;
|
||||
|
||||
a {
|
||||
color: #D5C8FF;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
// General typography tweaks
|
||||
|
||||
h1, h2 {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.party {
|
||||
width: 386px;
|
||||
margin-top: 4em;
|
||||
}
|
||||
|
||||
.team-based {
|
||||
background-image: url('../../assets/images/group-plans-static/group-management@3x.png');
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
height: 356px;
|
||||
width: 411px;
|
||||
margin-top: -2em;
|
||||
}
|
||||
|
||||
.group-management {
|
||||
background-image: url('../../assets/images/group-plans-static/team-based@3x.png');
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
height: 294px;
|
||||
width: 411px;
|
||||
}
|
||||
|
||||
.top-left, .top-right, .bot-left, .bot-right {
|
||||
width: 273px;
|
||||
height: 396px;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
background-image: url('../../assets/images/group-plans-static/top-left@3x.png');
|
||||
left: 4em;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
background-image: url('../../assets/images/group-plans-static/top-right@3x.png');
|
||||
right: 4em;
|
||||
height: 420px;
|
||||
}
|
||||
|
||||
.bot-left {
|
||||
background-image: url('../../assets/images/group-plans-static/bot-left@3x.png');
|
||||
left: 4em;
|
||||
bottom: 1em;
|
||||
}
|
||||
|
||||
.bot-right {
|
||||
background-image: url('../../assets/images/group-plans-static/bot-right@3x.png');
|
||||
right: 4em;
|
||||
bottom: 1em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 42px;
|
||||
color: #34313a;
|
||||
line-height: 1.17;
|
||||
color: $purple-300;
|
||||
font-size: 48px;
|
||||
line-height: 56px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 29px;
|
||||
color: #34313a;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: #6133b4;
|
||||
color: $gray-50;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: $gray-100;
|
||||
font-size: 20px;
|
||||
color: #878190;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.group-plan-static {
|
||||
margin-top: 6em;
|
||||
position: relative;
|
||||
}
|
||||
// Major layout elements
|
||||
|
||||
.row {
|
||||
margin-top: 10em;
|
||||
margin-bottom: 10em;
|
||||
}
|
||||
.bottom-banner {
|
||||
height: 152px;
|
||||
background-image: linear-gradient(rgba(97, 51, 180), rgba(79, 42, 147));
|
||||
padding-top: 32px;
|
||||
width: 100vw;
|
||||
|
||||
.text-col {
|
||||
margin-top: 3em;
|
||||
}
|
||||
&.static {
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.big-gem {
|
||||
width: 138.5px;
|
||||
&:not(.static) {
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-weight: normal;
|
||||
padding: 1em 2em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 4px;
|
||||
background-color: #6133b4;
|
||||
border-radius: 8px;
|
||||
background-color: $purple-300;
|
||||
box-shadow: inset 0 -4px 0 0 rgba(52, 49, 58, 0.4);
|
||||
font-size: 20px;
|
||||
color: #fff;
|
||||
line-height: 28px;
|
||||
|
||||
&.btn-primary:hover {
|
||||
background-color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.final-paragraph {
|
||||
width: 684px;
|
||||
margin-bottom: 11rem;
|
||||
}
|
||||
|
||||
.group-plan-page {
|
||||
max-width: 1440px;
|
||||
position: relative;
|
||||
|
||||
&.static {
|
||||
margin-top: 56px;
|
||||
}
|
||||
}
|
||||
|
||||
.pricing {
|
||||
color: #878190;
|
||||
color: $gray-100;
|
||||
font-size: 24px;
|
||||
|
||||
span {
|
||||
@@ -234,40 +210,103 @@
|
||||
}
|
||||
|
||||
.number {
|
||||
color: #1ca372;
|
||||
color: $green-10;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 16px;
|
||||
color: #a5a1ac;
|
||||
// One-off spacing adjustments
|
||||
|
||||
.gap-72 {
|
||||
gap: 72px;
|
||||
}
|
||||
|
||||
.mb-100 {
|
||||
margin-bottom: 100px !important;
|
||||
}
|
||||
|
||||
.mb-128 {
|
||||
margin-bottom: 128px !important;
|
||||
}
|
||||
|
||||
.w-448 {
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
// Images
|
||||
|
||||
.big-gem {
|
||||
width: 138.5px;
|
||||
}
|
||||
|
||||
.bot-left, .bot-right, .top-left, .top-right {
|
||||
width: 246px;
|
||||
height: 340px;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.bot-left {
|
||||
background-image: url('../../assets/images/group-plans-static/bot-left@3x.png');
|
||||
left: 48px;
|
||||
bottom: 48px;
|
||||
}
|
||||
|
||||
.bot-right {
|
||||
background-image: url('../../assets/images/group-plans-static/bot-right@3x.png');
|
||||
right: 48px;
|
||||
bottom: 48px;
|
||||
}
|
||||
|
||||
.party {
|
||||
width: 386px;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.top-left {
|
||||
background-image: url('../../assets/images/group-plans-static/top-left@3x.png');
|
||||
top: 48px;
|
||||
left: 48px;
|
||||
}
|
||||
|
||||
.top-right {
|
||||
background-image: url('../../assets/images/group-plans-static/top-right@3x.png');
|
||||
right: 48px;
|
||||
top: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { setup as setupPayments } from '@/libs/payments';
|
||||
import amazonPaymentsModal from '@/components/payments/amazonModal';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import AuthForm from '../auth/authForm.vue';
|
||||
import CreateGroupModalPages from '../group-plans/createGroupModalPages.vue';
|
||||
|
||||
import party from '../../assets/images/group-plans-static/party.svg';
|
||||
import GroupPlanCreationModal from '../group-plans/groupPlanCreationModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AuthForm,
|
||||
CreateGroupModalPages,
|
||||
amazonPaymentsModal,
|
||||
GroupPlanCreationModal,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
svg: {
|
||||
party,
|
||||
},
|
||||
modalTitle: this.$t('register'),
|
||||
modalOption: '',
|
||||
modalPage: 'account',
|
||||
modalTitle: this.$t('register'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
},
|
||||
upgradingGroup () {
|
||||
return this.$store.state.upgradingGroup;
|
||||
},
|
||||
user () {
|
||||
return this.$store.state.user?.data;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
@@ -278,11 +317,19 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
goToNewGroupPage () {
|
||||
this.$root.$emit('bv::show::modal', 'group-plan');
|
||||
},
|
||||
authenticate () {
|
||||
this.modalPage = 'purchaseGroup';
|
||||
this.$root.$emit('bv::hide::modal', 'group-plan');
|
||||
this.$root.$emit('bv::show::modal', 'create-group');
|
||||
},
|
||||
goToNewGroupPage () {
|
||||
if (this.isStaticPage && !this.user) {
|
||||
this.modalOption = 'static';
|
||||
return this.$root.$emit('bv::show::modal', 'group-plan');
|
||||
}
|
||||
if (this.upgradingGroup._id) {
|
||||
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });
|
||||
}
|
||||
return this.$root.$emit('bv::show::modal', 'create-group');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -139,13 +139,6 @@
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (max-width : 750px) {
|
||||
.login-button {
|
||||
margin: 0 auto !important;
|
||||
margin-top: 18px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.habitica-logo {
|
||||
height: 64px;
|
||||
margin: 28px auto 0px auto;
|
||||
@@ -165,7 +158,7 @@
|
||||
|
||||
nav.navbar {
|
||||
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right no-repeat;
|
||||
padding-left: 25px;
|
||||
padding-left: 24px;
|
||||
padding-right: 12.5px;
|
||||
height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
@@ -265,6 +258,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 750px) {
|
||||
.login-button {
|
||||
margin: 0 auto !important;
|
||||
margin-top: 18px !important;
|
||||
}
|
||||
.habitica-logo {
|
||||
margin: 4px auto 0px auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -781,9 +781,10 @@
|
||||
<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 { buildAppleAuthUrl } from '../../libs/auth';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
import googlePlay from '@/assets/images/home/google-play-badge.svg';
|
||||
import iosAppStore from '@/assets/images/home/ios-app-store.svg';
|
||||
import iphones from '@/assets/images/home/iphones.svg';
|
||||
@@ -804,6 +805,7 @@ import makeuseof from '@/assets/images/home/make-use-of.svg';
|
||||
import thenewyorktimes from '@/assets/images/home/the-new-york-times.svg';
|
||||
|
||||
export default {
|
||||
mixins: [sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -923,7 +925,9 @@ export default {
|
||||
groupInvite,
|
||||
});
|
||||
|
||||
window.location.href = this.$route.query.redirectTo || '/';
|
||||
const redirect = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
window.location.href = redirect;
|
||||
},
|
||||
playButtonClick () {
|
||||
this.$router.push('/register');
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
'white-header': $route.name === 'plans'
|
||||
}"
|
||||
/>
|
||||
<div class="static-wrapper">
|
||||
<div
|
||||
class="static-wrapper"
|
||||
:class="{ 'groups-bg': $route.name === 'groupPlans' }"
|
||||
>
|
||||
<router-view />
|
||||
</div>
|
||||
<div
|
||||
@@ -205,6 +208,13 @@
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.groups-bg {
|
||||
background-color: $white;
|
||||
background-image: url('../../assets/images/group-plans-static/top.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position-y: 56px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div
|
||||
v-once
|
||||
class="loading-spinner"
|
||||
:class="{'loading-spinner-purple': darkColor}"
|
||||
role="text"
|
||||
:aria-label="$t('loading')"
|
||||
>
|
||||
@@ -39,6 +40,10 @@
|
||||
border-color: $white transparent transparent transparent;
|
||||
}
|
||||
|
||||
.loading-spinner-purple div {
|
||||
border-color: $purple-200 transparent transparent transparent;
|
||||
}
|
||||
|
||||
.loading-spinner div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
@@ -58,3 +63,16 @@
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
darkColor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -318,13 +318,18 @@
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
td span {
|
||||
line-break: anywhere;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding-top: 0.35rem !important;
|
||||
padding-bottom: 0.35rem !important;
|
||||
}
|
||||
|
||||
.timestamp-column, .action-column {
|
||||
width: 20%;
|
||||
width: 27%;
|
||||
|
||||
}
|
||||
|
||||
.amount-column {
|
||||
@@ -332,7 +337,7 @@
|
||||
}
|
||||
|
||||
.note-column {
|
||||
width: 50%;
|
||||
width: 35%;
|
||||
}
|
||||
|
||||
.entry-action {
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import Vue from 'vue';
|
||||
import axios from 'axios';
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
import {
|
||||
ModalPlugin,
|
||||
DropdownPlugin,
|
||||
PopoverPlugin,
|
||||
FormPlugin,
|
||||
FormInputPlugin,
|
||||
FormRadioPlugin,
|
||||
TooltipPlugin,
|
||||
NavbarPlugin,
|
||||
CollapsePlugin,
|
||||
} from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import {
|
||||
@@ -12,7 +22,6 @@ import getStore from './store';
|
||||
import StoreModule from './libs/store';
|
||||
import './filters/registerGlobals';
|
||||
import i18n from './libs/i18n';
|
||||
import 'smartbanner.js/dist/smartbanner';
|
||||
|
||||
const IS_PRODUCTION = process.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
||||
|
||||
@@ -29,7 +38,15 @@ Vue.config.productionTip = IS_PRODUCTION;
|
||||
// window['habitica-i18n] is injected by the server
|
||||
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
|
||||
Vue.use(StoreModule);
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(ModalPlugin);
|
||||
Vue.use(DropdownPlugin);
|
||||
Vue.use(PopoverPlugin);
|
||||
Vue.use(FormPlugin);
|
||||
Vue.use(FormInputPlugin);
|
||||
Vue.use(FormRadioPlugin);
|
||||
Vue.use(TooltipPlugin);
|
||||
Vue.use(NavbarPlugin);
|
||||
Vue.use(CollapsePlugin);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mapState } from '@/libs/store';
|
||||
import encodeParams from '@/libs/encodeParams';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
const STRIPE_PUB_KEY = process.env.STRIPE_PUB_KEY;
|
||||
|
||||
@@ -198,6 +199,16 @@ export default {
|
||||
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
|
||||
throw checkoutSessionResult.error;
|
||||
}
|
||||
if (paymentType === 'groupPlan') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan create',
|
||||
eventAction: 'group plan create',
|
||||
eventCategory: 'behavior',
|
||||
demographics: appState.newGroup.demographics,
|
||||
type: appState.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
|
||||
alert(`Error while redirecting to Stripe: ${err.message}`);
|
||||
@@ -370,5 +381,20 @@ export default {
|
||||
window.alert(e.response.data.message); // eslint-disable-line no-alert
|
||||
}
|
||||
},
|
||||
stripeGroup (options = { group: {}, upgrade: false }) {
|
||||
const paymentData = {
|
||||
subscription: 'group_monthly',
|
||||
coupon: null,
|
||||
};
|
||||
|
||||
if (options.upgrade && options.group._id) {
|
||||
paymentData.groupId = options.group._id;
|
||||
paymentData.group = options.group;
|
||||
} else {
|
||||
paymentData.groupToCreate = options.group;
|
||||
}
|
||||
|
||||
this.redirectToStripe(paymentData);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
methods: {
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) {
|
||||
return '/';
|
||||
}
|
||||
if (process.env.TRUSTED_DOMAINS.split(',').includes(redirect)) {
|
||||
return redirect;
|
||||
}
|
||||
if (redirect.slice(0, 1) !== '/' || redirect.slice(1, 1) === '/') {
|
||||
return '/';
|
||||
}
|
||||
return redirect;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -131,7 +131,7 @@ input {
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import * as validator from 'validator';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
user: 'user.data',
|
||||
}),
|
||||
validEmail () {
|
||||
return validator.isEmail(this.updates.newEmail);
|
||||
return isEmail(this.updates.newEmail);
|
||||
},
|
||||
allowedToSave () {
|
||||
return !this.validEmail || this.updates.password.length === 0;
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import * as validator from 'validator';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import SaveCancelButtons from '../components/saveCancelButtons.vue';
|
||||
@@ -99,7 +99,7 @@ export default {
|
||||
return this.previousEmail !== this.updates.newEmail;
|
||||
},
|
||||
validEmail () {
|
||||
return validator.isEmail(this.updates.newEmail);
|
||||
return isEmail(this.updates.newEmail);
|
||||
},
|
||||
disallowedToSave () {
|
||||
return !this.emailChanged
|
||||
|
||||
@@ -208,7 +208,7 @@ table {
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import * as validator from 'validator';
|
||||
import isURL from 'validator/es/lib/isURL';
|
||||
import uuid from '@/../../common/script/libs/uuid';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
@@ -247,7 +247,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
isValidUrl (url) {
|
||||
return validator.isURL(url, {
|
||||
return isURL(url, {
|
||||
require_tld: true,
|
||||
require_protocol: true,
|
||||
protocols: ['http', 'https'],
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<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 />
|
||||
<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, 'groups-background': $route.fullPath === '/group-plans' }"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.casting-spell {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
flex: 1 0 auto;
|
||||
|
||||
&.groups-background {
|
||||
background-color: white;
|
||||
background-image: url('../assets/images/group-plans-static/top.svg');
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.notification {
|
||||
border-radius: 1000px;
|
||||
background-color: $green-10;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: .5em 1em;
|
||||
color: $white;
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: .9 !important;
|
||||
background-color: $purple-100 !important;
|
||||
}
|
||||
|
||||
/* Push progress bar above modals */
|
||||
#nprogress .bar {
|
||||
z-index: 1600 !important; /* Must stay above nav bar */
|
||||
}
|
||||
</style>
|
||||
|
||||
<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 { 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');
|
||||
|
||||
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,
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
amazonPaymentsModal,
|
||||
paymentsSuccessModal,
|
||||
subCancelModalConfirm,
|
||||
subCanceledModal,
|
||||
bugReportModal,
|
||||
bugReportSuccessModal,
|
||||
externalLinkModal,
|
||||
},
|
||||
mixins: [notifications, spellsMixin],
|
||||
data () {
|
||||
return {
|
||||
selectedItemToBuy: null,
|
||||
selectedSpellToBuy: null,
|
||||
|
||||
audioSource: null,
|
||||
audioSuffix: null,
|
||||
|
||||
loading: true,
|
||||
bannerHidden: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded']),
|
||||
...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 = `https://habitica-assets.s3.amazonaws.com/mobileApp/sounds/${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();
|
||||
});
|
||||
|
||||
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.$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,
|
||||
});
|
||||
|
||||
// Setup listener for title
|
||||
this.$store.watch(state => state.title, title => {
|
||||
document.title = title;
|
||||
});
|
||||
|
||||
// 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();
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
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
|
||||
});
|
||||
},
|
||||
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);
|
||||
},
|
||||
methods: {
|
||||
checkForBannedUser (error) {
|
||||
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const errorMessage = error.response.data.message;
|
||||
|
||||
// Case where user is not logged in
|
||||
if (!parseSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bannedMessage = this.$t('accountSuspended', {
|
||||
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
|
||||
userId: parseSettings.auth.apiId,
|
||||
});
|
||||
|
||||
if (errorMessage !== bannedMessage) return false;
|
||||
|
||||
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>
|
||||
@@ -37,15 +37,6 @@ export default function handleRedirect (to, from, next) {
|
||||
|
||||
const newGroup = newAppState.group;
|
||||
if (newGroup && newGroup._id) {
|
||||
// Handle new user signup
|
||||
if (newAppState.newSignup === true) {
|
||||
return next({
|
||||
name: 'groupPlanDetailTaskInformation',
|
||||
params: { groupId: newGroup._id },
|
||||
query: { showGroupOverview: 'true' },
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
name: 'groupPlanDetailTaskInformation',
|
||||
params: { groupId: newGroup._id },
|
||||
|
||||
@@ -22,6 +22,7 @@ const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall
|
||||
// Admin Panel
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin-panel/search');
|
||||
|
||||
// Except for tasks that are always loaded all the other main level
|
||||
// All the main level
|
||||
@@ -40,7 +41,7 @@ const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components
|
||||
|
||||
// Guilds & Parties
|
||||
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/groupPlan');
|
||||
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
|
||||
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
|
||||
|
||||
// Group Plans
|
||||
@@ -193,9 +194,19 @@ const router = new VueRouter({
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'adminPanelSearch',
|
||||
path: 'search/:userIdentifier',
|
||||
component: AdminPanelSearchPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'adminPanelUser',
|
||||
path: ':userIdentifier', // User ID or Username
|
||||
path: ':userIdentifier',
|
||||
component: AdminPanelUserPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function searchUsers (store, payload) {
|
||||
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
||||
|
||||
import * as adminPanel from './adminPanel';
|
||||
import * as common from './common';
|
||||
import * as user from './user';
|
||||
import * as tasks from './tasks';
|
||||
@@ -24,6 +25,7 @@ import * as faq from './faq';
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
|
||||
const actions = flattenAndNamespace({
|
||||
adminPanel,
|
||||
common,
|
||||
user,
|
||||
tasks,
|
||||
|
||||
@@ -76,6 +76,9 @@ const webpackPlugins = [
|
||||
if ((context.includes('sinon') || resource.includes('sinon') || context.includes('nise')) && nconf.get('TIME_TRAVEL_ENABLED') !== 'true') {
|
||||
return true;
|
||||
}
|
||||
if (context.includes('yargs')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
@@ -91,6 +94,28 @@ module.exports = {
|
||||
dependency: { not: ['url'] },
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
// Exclude transpiling `node_modules`, except `bootstrap-vue/src`
|
||||
exclude: /node_modules\/(?!bootstrap-vue\/src\/)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
// Exclude transpiling `node_modules`, except `bootstrap-vue/src`
|
||||
exclude: /node_modules\/(?!validator)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
@@ -102,6 +127,10 @@ module.exports = {
|
||||
stream: false,
|
||||
timers: require.resolve('timers-browserify'),
|
||||
},
|
||||
alias: {
|
||||
// Alias for using source of BootstrapVue
|
||||
'bootstrap-vue$': 'bootstrap-vue/src/index.js',
|
||||
},
|
||||
},
|
||||
plugins: webpackPlugins,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"backgrounds": "Фонови изображения",
|
||||
"background": "Фоново изображение",
|
||||
"backgroundShop": "Магазин за фонови изображения",
|
||||
"backgroundShopText": "Магазин за фонови изображения",
|
||||
"noBackground": "Няма избрано фоново изображение",
|
||||
"backgrounds062014": "КОМПЛЕКТ 1: юни 2014",
|
||||
"backgroundBeachText": "Плаж",
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
"keepIt": "Запазване",
|
||||
"removeIt": "Премахване",
|
||||
"brokenChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но то (или групата) е било изтрито. Какво бихте искали да направите с останалите задачи?",
|
||||
"keepThem": "Запазване на задачите",
|
||||
"removeThem": "Премахване на задачите",
|
||||
"challengeCompleted": "Това предизвикателство е приключило и победителят е <span class=\"badge\"><%- user %></span>! Какво искате да направите с останалите задачи?",
|
||||
"unsubChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но Вие сте се отписали от него. Какво искате да направите с останалите задачи?",
|
||||
"challenges": "Предизвикателства",
|
||||
@@ -23,25 +21,20 @@
|
||||
"createChallenge": "Създаване на предизвикателство",
|
||||
"createChallengeAddTasks": "Добавяне на задачи в предизвикателството",
|
||||
"createChallengeCloneTasks": "Клониране на задачите в предизвикателството",
|
||||
"addTaskToChallenge": "Добавяне на задача",
|
||||
"challengeTag": "Име на етикета",
|
||||
"prize": "Награда",
|
||||
"prizePopTavern": "Ако предизвикателството Ви може да бъде „спечелено“, може да наградите победителя с диаманти. Максималната награда е броят на Вашите диаманти. Забележка: Наградата не може да бъде променена по-късно, а цената на предизвикателствата в кръчмата не може да бъде възстановена, ако предизвикателството бъде прекратено.",
|
||||
"publicChallengesTitle": "Обществени предизвикателства",
|
||||
"officialChallenge": "Официално предизвикателство на Хабитика",
|
||||
"by": "от",
|
||||
"participants": "Участници: <%= membercount %>",
|
||||
"join": "Присъединяване",
|
||||
"exportChallengeCSV": "Изнасяне като CSV",
|
||||
"challengeCreated": "Предизвикателството е създадено",
|
||||
"sureDelCha": "Наистина ли искате да изтриете това предизвикателство?",
|
||||
"sureDelChaTavern": "Наистина ли искате да изтриете това предизвикателство? Диамантите Ви няма да бъдат възстановени.",
|
||||
"keepTasks": "Запазване на задачите",
|
||||
"owned": "Собствени",
|
||||
"not_owned": "Чужди",
|
||||
"not_participating": "Не участвате",
|
||||
"clone": "Копиране",
|
||||
"congratulations": "Поздравления!",
|
||||
"hurray": "Ура!",
|
||||
"noChallengeOwner": "без притежател",
|
||||
"challengeMemberNotFound": "Потребителят не е намерен сред участниците в предизвикателството",
|
||||
@@ -62,7 +55,6 @@
|
||||
"myChallenges": "Моите предизвикателства",
|
||||
"findChallenges": "Разглеждане на предизвикателствата",
|
||||
"noChallengeTitle": "Нямате никакви предизвикателства.",
|
||||
"challengeDescription1": "Предизвикателствата са обществени събития, в които играчите се състезават и печелят награди като изпълняват няколко свързани по някакъв начин задачи.",
|
||||
"challengeDescription2": "Открийте препоръчани за Вас предизвикателства според интересите Ви, разгледайте обществените предизвикателства на Хабитика, или създайте свои собствени предизвикателства.",
|
||||
"noChallengeMatchFilters": "Не можем да открием съответстващи предизвикателства.",
|
||||
"createdBy": "Създадено от",
|
||||
|
||||