mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-14 11:28:57 -05:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aa343d390 | |||
| d80c43c82a | |||
| 80e4b8617a | |||
| b3fac011a9 | |||
| 8f4d871911 | |||
| 19373ce84d | |||
| 55bfca20d9 | |||
| 2ee2b05d1c | |||
| 3d3db1bdd9 | |||
| a5dff99fa1 | |||
| 25435218e1 | |||
| 7746049bb4 | |||
| 036bf43cf3 | |||
| c4e0127a37 | |||
| 7ae059e243 | |||
| 81b9a0b92d | |||
| a6e87452a6 | |||
| c40d384913 | |||
| 65144aef28 | |||
| cc7683a871 | |||
| 31b2781333 | |||
| d37d3bc5ac | |||
| ef3a28791e | |||
| c3c2607bca | |||
| 7a6d64f158 | |||
| 836e63246d | |||
| 61585b2549 | |||
| 07275bd522 | |||
| 74fc543ef2 | |||
| 8c90e5472b | |||
| c8d9ba6c8e | |||
| 1675c2749b | |||
| e85a2bae14 | |||
| 3355500fba | |||
| 486f15df0f | |||
| f0b6b5611c | |||
| 7e45c79714 | |||
| 8da6065355 | |||
| a212363bda | |||
| 2e19e73b9e | |||
| 1047b0e03b | |||
| 159f850bd1 | |||
| 42083efb7e | |||
| f21e800b0b | |||
| 40122e5621 | |||
| 0ae19d9107 | |||
| 68bfebcf30 | |||
| 3e93911e70 | |||
| 4ea8636f03 | |||
| 9f97a09b8c | |||
| eccc115b73 | |||
| 2b26eb2bd1 | |||
| 8e042cabc4 | |||
| 8abe167848 | |||
| 3414f962e2 | |||
| 1b68e6d4d3 | |||
| 5dd9711413 |
@@ -82,7 +82,7 @@ jobs:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:sanity
|
||||
|
||||
|
||||
common:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
@@ -129,13 +129,13 @@ jobs:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
- run: npm run test:content
|
||||
|
||||
|
||||
api-unit:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -144,11 +144,13 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -158,15 +160,17 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api:unit
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
api-v3-integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -176,10 +180,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -189,15 +194,18 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api-v3:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
api-v4-integration:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [21.x]
|
||||
mongodb-version: [4.2]
|
||||
mongodb-version: [7.0]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -207,10 +215,11 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
|
||||
uses: supercharge/mongodb-github-action@1.3.0
|
||||
uses: supercharge/mongodb-github-action@1.11.0
|
||||
with:
|
||||
mongodb-version: ${{ matrix.mongodb-version }}
|
||||
mongodb-replica-set: rs
|
||||
|
||||
- run: sudo apt update
|
||||
- run: sudo apt -y install libkrb5-dev
|
||||
- run: cp config.json.example config.json
|
||||
@@ -220,6 +229,7 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
NODE_ENV: test
|
||||
|
||||
- run: npm run test:api-v4:integration
|
||||
env:
|
||||
REQUIRES_SERVER=true: true
|
||||
|
||||
+1
-1
@@ -47,5 +47,5 @@ webpack.webstorm.config
|
||||
|
||||
# mongodb replica set for local dev
|
||||
mongodb-*.tgz
|
||||
/mongodb-data*
|
||||
/mongodb-*
|
||||
/.nyc_output
|
||||
|
||||
+2
-3
@@ -46,7 +46,7 @@
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs&directConnection=true&readPreference=secondary",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
@@ -75,7 +75,6 @@
|
||||
"S3_ACCESS_KEY_ID": "accessKeyId",
|
||||
"S3_BUCKET": "bucket",
|
||||
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||
@@ -90,7 +89,7 @@
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs&directConnection=true&readPreference=secondary",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"WEB_CONCURRENCY": 1
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
services:
|
||||
client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev"]
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
- /usr/src/habitica/website/client/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
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:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
habitica:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,23 @@
|
||||
networks:
|
||||
mongodb-network:
|
||||
name: "mongodb-network"
|
||||
driver: bridge
|
||||
services:
|
||||
mongodb:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb-only"
|
||||
networks:
|
||||
- mongodb-network
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
@@ -0,0 +1,23 @@
|
||||
networks:
|
||||
mongodb-network:
|
||||
name: "mongodb-network"
|
||||
driver: bridge
|
||||
services:
|
||||
mongodb:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb-test"
|
||||
networks:
|
||||
- mongodb-network
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker-testing:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
+43
-22
@@ -1,35 +1,56 @@
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
client:
|
||||
build: .
|
||||
networks:
|
||||
- habitica
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
ports:
|
||||
- "8080:8080"
|
||||
command: ["npm", "run", "client:dev"]
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "run", "client:dev:docker"]
|
||||
depends_on:
|
||||
- server
|
||||
|
||||
server:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- BASE_URL=http://server:3000
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "5173:5173"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
- /usr/src/habitica/website/client/node_modules
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./Dockerfile-Dev
|
||||
command: ["npm", "start"]
|
||||
depends_on:
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_DB_URI=mongodb://mongo/habitrpg
|
||||
depends_on:
|
||||
- mongo
|
||||
|
||||
mongo:
|
||||
image: mongo:3.6
|
||||
ports:
|
||||
- "27017:27017"
|
||||
networks:
|
||||
- habitica
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- .:/usr/src/habitica
|
||||
- /usr/src/habitica/node_modules
|
||||
mongo:
|
||||
image: "mongo:7.0"
|
||||
container_name: "habitica-mongodb"
|
||||
networks:
|
||||
- habitica
|
||||
hostname: "mongodb"
|
||||
ports:
|
||||
- "27017:27017"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "./mongodb-data-docker:/data/db"
|
||||
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
networks:
|
||||
habitica:
|
||||
|
||||
+12
-9
@@ -5,7 +5,7 @@ import path from 'path';
|
||||
import babel from 'gulp-babel';
|
||||
import os from 'os';
|
||||
import fs from 'fs';
|
||||
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import spawn from 'cross-spawn';
|
||||
import clean from 'rimraf';
|
||||
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
@@ -35,7 +35,7 @@ gulp.task('build:prod', gulp.series(
|
||||
// When used on windows `run-rs` must first be run without the `--keep` option
|
||||
// in order to be setup correctly, afterwards it can be used.
|
||||
|
||||
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
|
||||
const MONGO_PATH = path.join(__dirname, '/../mongodb-data-docker/');
|
||||
|
||||
gulp.task('build:prepare-mongo', async () => {
|
||||
if (fs.existsSync(MONGO_PATH)) {
|
||||
@@ -51,29 +51,32 @@ gulp.task('build:prepare-mongo', async () => {
|
||||
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
|
||||
|
||||
// use run-rs without --keep, kill it as soon as the replica set starts
|
||||
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
|
||||
const dockerMongoProcess = spawn('npm', ['run', 'docker:mongo:dev']);
|
||||
|
||||
for await (const chunk of runRsProcess.stdout) {
|
||||
let manuallyStopped = false;
|
||||
|
||||
for await (const chunk of dockerMongoProcess.stdout) {
|
||||
const stringChunk = chunk.toString();
|
||||
console.log(stringChunk); // eslint-disable-line no-console
|
||||
// kills the process after the replica set is setup
|
||||
if (stringChunk.includes('Started replica set')) {
|
||||
if (stringChunk.includes('mongod startup complete')) {
|
||||
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
|
||||
runRsProcess.kill();
|
||||
dockerMongoProcess.kill();
|
||||
manuallyStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
let error = '';
|
||||
for await (const chunk of runRsProcess.stderr) {
|
||||
for await (const chunk of dockerMongoProcess.stderr) {
|
||||
const stringChunk = chunk.toString();
|
||||
error += stringChunk;
|
||||
}
|
||||
|
||||
const exitCode = await new Promise(resolve => {
|
||||
runRsProcess.on('close', resolve);
|
||||
dockerMongoProcess.on('close', resolve);
|
||||
});
|
||||
|
||||
if (exitCode || error.length > 0) {
|
||||
if (!manuallyStopped && (exitCode || error.length > 0)) {
|
||||
// remove any leftover files
|
||||
clean.sync(MONGO_PATH);
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ gulp.task('test:prepare:mongo', cb => {
|
||||
const mongooseOptions = getDefaultConnectionOptions();
|
||||
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
|
||||
|
||||
console.info({
|
||||
mongooseOptions,
|
||||
connectionUrl,
|
||||
});
|
||||
|
||||
mongoose.connect(connectionUrl, mongooseOptions)
|
||||
.then(() => mongoose.connection.dropDatabase())
|
||||
.then(() => mongoose.connection.close()).then(() => {
|
||||
|
||||
+1
-1
Submodule habitica-images updated: 8a96a0ff62...32a4678c6b
Generated
+279
-708
File diff suppressed because it is too large
Load Diff
+9
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.44.2",
|
||||
"version": "5.47.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -39,9 +39,9 @@
|
||||
"gulp-filter": "^7.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"habitica-markdown": "^4.1.0",
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"helmet": "^8.1.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
@@ -76,7 +76,6 @@
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
@@ -103,13 +102,16 @@
|
||||
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
|
||||
"sprites": "gulp sprites:compile",
|
||||
"client:dev": "cd website/client && npm run serve",
|
||||
"client:dev:docker": "cd website/client && npm run serve:docker",
|
||||
"client:build": "cd website/client && npm run build",
|
||||
"client:unit": "cd website/client && npm run test:unit",
|
||||
"start": "node --watch ./website/server/index.js",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "node --watch --inspect ./website/server/index.js",
|
||||
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"docker:aio": "docker compose -f docker-compose.yml up",
|
||||
"docker:mongo:dev": "docker compose -f docker-compose.mongo-only.yml up",
|
||||
"docker:mongo:dev:down": "docker compose -f docker-compose.mongo-only.yml down",
|
||||
"docker:mongo:test": "docker compose -f docker-compose.mongo-test-local.yml up",
|
||||
"mongo:test": "node scripts/start-local-mongo.mjs --test-db",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
@@ -125,7 +127,6 @@
|
||||
"monk": "^7.3.4",
|
||||
"nyc": "^15.1.0",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -66,13 +66,15 @@ describe('Amazon Payments - Cancel Subscription', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
@@ -30,12 +30,14 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
@@ -246,11 +248,6 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Add existing users
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Set expected amount
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
|
||||
@@ -128,11 +128,12 @@ describe('Purchasing a group plan for group', () => {
|
||||
expect(publicGroup.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = publicGroup._id;
|
||||
|
||||
// Public Guilds are no longer even findable
|
||||
await expect(api.createSubscription(data))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyPrivateGuildsCanUpgrade'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
|
||||
const updatedGroup = await Group.findById(publicGroup._id).exec();
|
||||
|
||||
@@ -30,13 +30,15 @@ describe('paypal - subscribeCancel', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ describe('Stripe - Checkout', () => {
|
||||
const group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
const groupId = group._id;
|
||||
@@ -376,11 +376,13 @@ describe('Stripe - Checkout', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
});
|
||||
|
||||
it('throws if user is not allowed to change group plan', async () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
@@ -315,12 +315,14 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
@@ -50,5 +50,59 @@ describe('UserNotification Model', () => {
|
||||
expect(safeNotifications[0].type).to.equal('NEW_CHAT_MESSAGE');
|
||||
expect(safeNotifications[0].id).to.equal('123');
|
||||
});
|
||||
|
||||
it('removes duplicate STREAK_ACHIEVEMENT notifications', () => {
|
||||
// Fixes issue #13325 - Users receiving duplicate streak achievement notifications
|
||||
const notifications = [
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 123,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 456,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'CRON',
|
||||
id: 789,
|
||||
data: {},
|
||||
}), // different type, should be kept
|
||||
];
|
||||
|
||||
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
|
||||
expect(safeNotifications.length).to.equal(2);
|
||||
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
|
||||
expect(safeNotifications[0].id).to.equal('123');
|
||||
expect(safeNotifications[1].type).to.equal('CRON');
|
||||
expect(safeNotifications[1].id).to.equal('789');
|
||||
});
|
||||
|
||||
it('handles multiple STREAK_ACHIEVEMENT duplicates correctly', () => {
|
||||
// Test case: 3 duplicate STREAK_ACHIEVEMENT notifications
|
||||
const notifications = [
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 111,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 222,
|
||||
data: {},
|
||||
}),
|
||||
new UserNotification({
|
||||
type: 'STREAK_ACHIEVEMENT',
|
||||
id: 333,
|
||||
data: {},
|
||||
}),
|
||||
];
|
||||
|
||||
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
|
||||
expect(safeNotifications.length).to.equal(1);
|
||||
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
|
||||
expect(safeNotifications[0].id).to.equal('111'); // Keep first one
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('POST /challenges/:challengeId/join', () => {
|
||||
it('returns error when challengeId is not a valid UUID', async () => {
|
||||
@@ -27,6 +29,37 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('public Guild', () => {
|
||||
let group;
|
||||
let groupLeader;
|
||||
let members;
|
||||
let challenge;
|
||||
before(async () => {
|
||||
({ group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
}));
|
||||
challenge = await generateChallenge(groupLeader, group);
|
||||
// Creation API is shut down, we need to simulate an extant public group
|
||||
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
|
||||
});
|
||||
|
||||
it('returns error when challengeId is in an old public Guild', async () => {
|
||||
const authorizedUser = members[0]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
await expect(authorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a valid challenge', () => {
|
||||
let groupLeader;
|
||||
let group;
|
||||
@@ -66,6 +99,15 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
expect(res.name).to.equal(challenge.name);
|
||||
});
|
||||
|
||||
it('succeeds when it\'s a Tavern challenge, even if the user isn\'t a "member" of Tavern', async () => {
|
||||
const tavern = await groupLeader.get(`/groups/${TAVERN_ID}`);
|
||||
const tavernChallenge = await generateChallenge(groupLeader, tavern, { prize: 1 });
|
||||
const generalUser = await generateUser();
|
||||
|
||||
const res = await generalUser.post(`/challenges/${tavernChallenge._id}/join`);
|
||||
expect(res.name).to.equal(tavernChallenge.name);
|
||||
});
|
||||
|
||||
it('returns challenge data', async () => {
|
||||
const res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('GET /groups/:groupId/chat', () => {
|
||||
|
||||
it('returns error if user attempts to fetch a sunset Guild', async () => {
|
||||
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,9 +121,9 @@ describe('POST /chat/:chatId/like', () => {
|
||||
|
||||
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
xdescribe('GET /export/avatar-:memberId.html', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('handles non-existing members', async () => {
|
||||
const dummyId = generateUUID();
|
||||
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', { userId: dummyId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an html page', async () => {
|
||||
const res = await user.get(`/export/avatar-${user._id}.html`);
|
||||
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
// TODO how to test this route since it points to a file on AWS s3?
|
||||
|
||||
describe('GET /export/avatar-:memberId.png', () => {});
|
||||
@@ -38,7 +38,7 @@ describe('GET /export/inbox.html', () => {
|
||||
it('renders the markdown messages as html', async () => {
|
||||
const res = await user.get('/export/inbox.html');
|
||||
|
||||
expect(res).to.include('img class="habitica-emoji"');
|
||||
expect(res).to.include('😄');
|
||||
expect(res).to.include('<h1>Hello!</h1>');
|
||||
expect(res).to.include('<li>list 1</li>');
|
||||
});
|
||||
@@ -46,7 +46,7 @@ describe('GET /export/inbox.html', () => {
|
||||
it('sorts messages from newest to oldest', async () => {
|
||||
const res = await user.get('/export/inbox.html');
|
||||
|
||||
const emojiPosition = res.indexOf('img class="habitica-emoji"');
|
||||
const emojiPosition = res.indexOf('😄');
|
||||
const headingPosition = res.indexOf('<h1>Hello!</h1>');
|
||||
const listPosition = res.indexOf('<li>list 1</li>');
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import md from 'habitica-markdown';
|
||||
|
||||
describe('habiticaMarkdown emoji plugin', () => {
|
||||
it('renders standard emoji as Unicode', () => {
|
||||
const result = md.render(':smile:');
|
||||
expect(result).to.include('😄');
|
||||
expect(result).not.to.include('img');
|
||||
});
|
||||
|
||||
it('renders thumbsup emoji as Unicode', () => {
|
||||
const result = md.render(':thumbsup:');
|
||||
expect(result).to.include('👍');
|
||||
});
|
||||
|
||||
it('renders +1 emoji as Unicode', () => {
|
||||
const result = md.render(':+1:');
|
||||
expect(result).to.include('👍');
|
||||
});
|
||||
|
||||
it('renders melior as an img tag', () => {
|
||||
const result = md.render(':melior:');
|
||||
expect(result).to.include('<img class="habitica-emoji"');
|
||||
expect(result).to.include('src="https://s3.amazonaws.com/habitica-assets/cdn/emoji/melior.png"');
|
||||
expect(result).to.include('alt="melior"');
|
||||
});
|
||||
|
||||
it('does NOT convert emoji inside markdown links', () => {
|
||||
const result = md.render('[:smile: link](http://example.com)');
|
||||
expect(result).to.include(':smile: link');
|
||||
expect(result).not.to.include('😄');
|
||||
});
|
||||
|
||||
it('converts emoji outside of links normally', () => {
|
||||
const result = md.render(':smile: [link](http://example.com)');
|
||||
expect(result).to.include('😄');
|
||||
expect(result).to.include('link');
|
||||
});
|
||||
|
||||
it('leaves removed custom emoji (bowtie) as literal text', () => {
|
||||
const result = md.render(':bowtie:');
|
||||
expect(result).to.include(':bowtie:');
|
||||
expect(result).not.to.include('img');
|
||||
});
|
||||
|
||||
it('leaves unknown shortcodes as literal text', () => {
|
||||
const result = md.render(':nonexistent_emoji_xyz:');
|
||||
expect(result).to.include(':nonexistent_emoji_xyz:');
|
||||
});
|
||||
|
||||
it('renders new emoji not in the old dataset', () => {
|
||||
const result = md.render(':yawning_face:');
|
||||
expect(result).to.include('🥱');
|
||||
});
|
||||
|
||||
it('supports unsafeHTMLRender', () => {
|
||||
const result = md.unsafeHTMLRender('<b>bold</b> :smile:');
|
||||
expect(result).to.include('<b>bold</b>');
|
||||
expect(result).to.include('😄');
|
||||
});
|
||||
|
||||
it('supports renderWithMentions', () => {
|
||||
const result = md.renderWithMentions(':smile: @testuser', { userName: 'testuser' });
|
||||
expect(result).to.include('😄');
|
||||
expect(result).to.include('at-text');
|
||||
expect(result).to.include('at-highlight');
|
||||
});
|
||||
});
|
||||
@@ -211,22 +211,32 @@ describe('shared.ops.rebirth', () => {
|
||||
expect(user.achievements.rebirthLevel).to.equal(2);
|
||||
});
|
||||
|
||||
it('does not increment rebirth achievements when level is lower than previous', async () => {
|
||||
it('increments rebirth achievements even when level is lower than previous', async () => {
|
||||
user.stats.lvl = 2;
|
||||
user.achievements.rebirths = 1;
|
||||
user.achievements.rebirthLevel = 3;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
expect(user.achievements.rebirths).to.equal(1);
|
||||
expect(user.achievements.rebirths).to.equal(2);
|
||||
expect(user.achievements.rebirthLevel).to.equal(3);
|
||||
});
|
||||
|
||||
it('always increments rebirth achievements when level is MAX_LEVEL', async () => {
|
||||
it('updates rebirthLevel when current level is higher than previous', async () => {
|
||||
user.stats.lvl = 5;
|
||||
user.achievements.rebirths = 1;
|
||||
user.achievements.rebirthLevel = 3;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
expect(user.achievements.rebirths).to.equal(2);
|
||||
expect(user.achievements.rebirthLevel).to.equal(5);
|
||||
});
|
||||
|
||||
it('increments rebirth achievements when level is MAX_LEVEL', async () => {
|
||||
user.stats.lvl = MAX_LEVEL;
|
||||
user.achievements.rebirths = 1;
|
||||
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
|
||||
user.achievements.rebirthLevel = MAX_LEVEL + 1;
|
||||
user.achievements.rebirthLevel = MAX_LEVEL;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
@@ -234,11 +244,10 @@ describe('shared.ops.rebirth', () => {
|
||||
expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL);
|
||||
});
|
||||
|
||||
it('always increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
|
||||
it('increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
|
||||
user.stats.lvl = MAX_LEVEL + 1;
|
||||
user.achievements.rebirths = 1;
|
||||
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
|
||||
user.achievements.rebirthLevel = MAX_LEVEL + 2;
|
||||
user.achievements.rebirthLevel = MAX_LEVEL;
|
||||
|
||||
await rebirth(user);
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getMatchingSwap, makeSubstitutionMap } from '../../website/common/script/content/constants/aprilFools';
|
||||
|
||||
describe('April Fools', () => {
|
||||
describe('getMatchingSwap', () => {
|
||||
it('returns Veggie for 2020', () => {
|
||||
const swap = getMatchingSwap(new Date('2020-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
it('returns Alien for 2026', () => {
|
||||
const swap = getMatchingSwap(new Date('2026-04-01'));
|
||||
expect(swap).to.equal('Alien');
|
||||
});
|
||||
it('Cycles through swaps correctly', () => {
|
||||
const swap = getMatchingSwap(new Date('2027-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeSubstitutionMap', () => {
|
||||
it('returns correct substitution for Veggie', () => {
|
||||
const substitutions = makeSubstitutionMap('Veggie');
|
||||
expect(substitutions.pets['Pet-Wolf-']).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets['Pet-TigerCub-']).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Veggie');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Veggie');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets.noPetIOS).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Veggie');
|
||||
});
|
||||
|
||||
it('returns correct substitution for Cryptid', () => {
|
||||
const substitutions = makeSubstitutionMap('Cryptid');
|
||||
expect(substitutions.pets['Pet-Fox-']).to.equal('Pet-Fox-Cryptid');
|
||||
expect(substitutions.pets['Pet-FlyingPig-']).to.equal('Pet-FlyingPig-Cryptid');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Cryptid');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Cryptid');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Cryptid');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Cryptid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export async function getProperty (collectionName, id, path) {
|
||||
// Specifically helpful for the GET /groups tests,
|
||||
// resets the db to an empty state and creates a tavern document
|
||||
export async function resetHabiticaDB () {
|
||||
console.info('Resetting Habitica DB');
|
||||
const groups = mongoose.connection.db.collection('groups');
|
||||
const users = mongoose.connection.db.collection('users');
|
||||
return mongoose.connection.dropDatabase()
|
||||
|
||||
@@ -12,20 +12,12 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
// TODO find a way to let eslint understand webpack aliases
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': 'off',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
// this creates issues with the current way we have to push the process.env vars to webpack
|
||||
// https://github.com/eslint/eslint/issues/14918
|
||||
// https://github.com/webpack/webpack/issues/5392
|
||||
// off for now, because any eslint --fix will then still do it anyway
|
||||
// maybe this can be turned on again once we switch to newer vue/vite
|
||||
// Important! process.env.XYZ should not be destructured
|
||||
'prefer-destructuring': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
Generated
+4655
-3035
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vite",
|
||||
"serve:docker": "npx vite --host 0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest run",
|
||||
@@ -27,7 +28,7 @@
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"habitica-markdown": "^4.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.1",
|
||||
@@ -45,7 +46,6 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -59,8 +59,6 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default {
|
||||
}
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
if (error.response.status === 404
|
||||
&& error.response.config.method === 'get'
|
||||
&& error.response.config.url.indexOf('/api/v4/groups/party') !== -1) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
const errorData = error.response.data;
|
||||
|
||||
@@ -22,8 +22,15 @@
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
|
||||
.Pet_HatchingPotion_Alien {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
@@ -52,6 +59,10 @@
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
|
||||
@@ -1060,6 +1060,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_elven_citadel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elven_citadel.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_enchanted_music_room {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
|
||||
width: 141px;
|
||||
@@ -1796,6 +1801,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_a_strange_planet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_strange_planet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_tree_branch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
|
||||
width: 141px;
|
||||
@@ -1931,6 +1941,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_riding_a_comet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_riding_a_comet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_rime_ice {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rime_ice.png');
|
||||
width: 141px;
|
||||
@@ -2427,6 +2442,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_waterfall_with_rainbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_waterfall_with_rainbow.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_wedding_arch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_wedding_arch.png');
|
||||
width: 141px;
|
||||
@@ -29800,6 +29820,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -30075,6 +30100,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -30385,6 +30415,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_floppyYellowHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyYellowHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_flutteryWig {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
|
||||
width: 114px;
|
||||
@@ -30705,6 +30740,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_verdantArmingCap {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_verdantArmingCap.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_vermilionArcherHelm {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_vermilionArcherHelm.png');
|
||||
width: 90px;
|
||||
@@ -31120,6 +31160,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softYellowPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softYellowPillow.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_spanishGuitar {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
|
||||
width: 114px;
|
||||
@@ -31170,6 +31215,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_verdantBanner {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_verdantBanner.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_vikingShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_vikingShield.png');
|
||||
width: 90px;
|
||||
@@ -31440,6 +31490,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_hattersSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_hattersSuit.png');
|
||||
width: 114px;
|
||||
@@ -31715,6 +31770,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softYellowSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softYellowSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_springPetalYukata {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_springPetalYukata.png');
|
||||
width: 114px;
|
||||
@@ -34125,11 +34185,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34140,11 +34210,31 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202604 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202604.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34155,6 +34245,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202603 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202603.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_201402 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
|
||||
width: 90px;
|
||||
@@ -36275,6 +36370,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36595,6 +36710,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -36780,6 +36915,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -37015,6 +37165,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -37255,6 +37425,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_spring2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_springHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_springHealer.png');
|
||||
width: 90px;
|
||||
@@ -53038,6 +53228,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -53528,6 +53723,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54318,6 +54518,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54813,6 +55018,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Amber.png');
|
||||
width: 81px;
|
||||
@@ -55148,6 +55358,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Amber.png');
|
||||
width: 81px;
|
||||
@@ -55928,6 +56143,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -56533,6 +56753,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -57928,6 +58153,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -58573,6 +58803,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Amber.png');
|
||||
width: 81px;
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 375 B |
@@ -58,6 +58,11 @@ h3.markdown {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.emoji-native {
|
||||
font-size: 0.85em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 0 16px;
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.d-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="330" height="80" viewBox="0 0 330 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.797 60.5502C165.534 61.8466 171.631 62.7536 178.221 63.1767C208.94 65.1492 233.733 56.6838 260.68 47.483C282.33 40.091 305.37 32.2243 333.99 28.9144L336 16.3018C260.81 7.08865 233.373 23.1672 205.362 39.5825C192.037 47.3908 178.583 55.2753 159.797 60.5502Z" fill="#BDA8FF"/>
|
||||
<path d="M0 80L331.948 79.9998V29.1594C268.976 36.9871 233.03 66.6959 178.221 63.1767C112.951 58.9858 95.9516 7.31934 0.000104656 0L0 80Z" fill="#925CF3"/>
|
||||
<path d="M203.54 40.6496C166.339 36.8525 141.531 39.6251 122.334 45.4666C133.94 51.8989 145.792 57.3851 159.797 60.5502C177.727 55.5155 190.801 48.1036 203.54 40.6496Z" fill="#D5C8FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 803 B |
@@ -1,53 +1,228 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="rebirth"
|
||||
:title="$t('modalAchievement')"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
size="sm"
|
||||
:hide-header="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="col-12">
|
||||
<!-- @TODO: +achievementAvatar('sun',0)--><achievement-avatar class="avatar" />
|
||||
</div><div class="col-6 offset-3 text-center">
|
||||
<div v-if="user.achievements.rebirthLevel < 100">
|
||||
{{ $t('rebirthAchievement', {
|
||||
number: user.achievements.rebirths,
|
||||
level: user.achievements.rebirthLevel}) }}
|
||||
</div><div v-if="user.achievements.rebirthLevel >= 100">
|
||||
{{ $t('rebirthAchievement100', {number: user.achievements.rebirths}) }}
|
||||
</div><br><button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('huzzah') }}
|
||||
</button>
|
||||
<div
|
||||
class="close-x"
|
||||
@click.stop="close()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon svg-close"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<div class="content text-center">
|
||||
<h2
|
||||
v-once
|
||||
class="header"
|
||||
>
|
||||
{{ $t('rebirthNewAchievement') }}
|
||||
</h2>
|
||||
<div class="d-flex align-items-center justify-content-center icon-area">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon sparkles mirror"
|
||||
v-html="icons.starGroup"
|
||||
></div>
|
||||
<Sprite
|
||||
class="achievement-icon"
|
||||
image-name="achievement-sun2x"
|
||||
/>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon sparkles"
|
||||
v-html="icons.starGroup"
|
||||
></div>
|
||||
</div>
|
||||
</div><achievement-footer />
|
||||
<p class="subtitle">
|
||||
{{ $t('rebirthNewAdventure') }}
|
||||
</p>
|
||||
<p
|
||||
class="description"
|
||||
v-html="achievementText"
|
||||
></p>
|
||||
<p
|
||||
v-once
|
||||
class="stack-info"
|
||||
>
|
||||
{{ $t('rebirthStackInfo') }}
|
||||
</p>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('onwards') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
slot="modal-footer"
|
||||
class="footer-wave"
|
||||
v-html="icons.purpleWaves"
|
||||
></div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
<style lang="scss">
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#rebirth {
|
||||
.modal-dialog {
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.content {
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
color: $purple-200;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon-area {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
width: 40px;
|
||||
height: 64px;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.close-x {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
|
||||
&:hover .svg-close {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.svg-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.achievement-icon {
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 12px;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
margin-bottom: 12px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.stack-info {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.footer-wave {
|
||||
width: 100%;
|
||||
|
||||
::v-deep svg {
|
||||
display: block;
|
||||
width: calc(100% + 8px);
|
||||
height: auto;
|
||||
margin: 0 -4px -4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import closeIcon from '@/assets/svg/close.svg?raw';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import starGroup from '@/assets/svg/star-group.svg?raw';
|
||||
import purpleWaves from '@/assets/svg/purple-waves.svg?raw';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
Sprite,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
starGroup,
|
||||
purpleWaves,
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
achievementText () {
|
||||
const rebirths = this.user.achievements.rebirths || 0;
|
||||
const level = this.user.achievements.rebirthLevel || 0;
|
||||
|
||||
if (level >= 100) {
|
||||
return this.$t('rebirthAchievement100', { number: rebirths, level });
|
||||
}
|
||||
|
||||
if (rebirths === 1) {
|
||||
return this.$t('rebirthAchievement', { number: rebirths, level });
|
||||
}
|
||||
|
||||
return this.$t('rebirthAchievementPlural', { number: rebirths, level });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
|
||||
@@ -1,41 +1,186 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="rebirth-enabled"
|
||||
:title="$t('rebirthNew')"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
size="sm"
|
||||
:hide-header="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="col-12">
|
||||
<div class="rebirth_orb"></div>
|
||||
<p>
|
||||
<span>{{ $t('rebirthUnlock') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="close-x"
|
||||
@click.stop="close()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon svg-close"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
<div class="content text-center">
|
||||
<h2
|
||||
v-once
|
||||
class="header"
|
||||
>
|
||||
{{ $t('rebirthUnlockedNewItem') }}
|
||||
</h2>
|
||||
<div class="d-flex align-items-center justify-content-center icon-area">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon sparkles mirror"
|
||||
v-html="icons.starGroup"
|
||||
></div>
|
||||
<img
|
||||
class="orb-icon"
|
||||
src="@/assets/images/rebirth-orb.png"
|
||||
alt="Orb of Rebirth"
|
||||
>
|
||||
{{ $t('close') }}
|
||||
</button>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon sparkles"
|
||||
v-html="icons.starGroup"
|
||||
></div>
|
||||
</div>
|
||||
<p
|
||||
v-once
|
||||
class="subtitle"
|
||||
>
|
||||
{{ $t('rebirthUnlockedOrb') }}
|
||||
</p>
|
||||
<p
|
||||
v-once
|
||||
class="description"
|
||||
>
|
||||
{{ $t('rebirthUnlockedDesc') }}
|
||||
</p>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('onwards') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
slot="modal-footer"
|
||||
class="clearfix"
|
||||
></div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rebirth_orb {
|
||||
margin: 0 auto;
|
||||
<style lang="scss">
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#rebirth-enabled {
|
||||
.modal-dialog {
|
||||
width: 330px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.content {
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
color: $purple-200;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.icon-area {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
width: 40px;
|
||||
height: 64px;
|
||||
|
||||
&.mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.close-x {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
|
||||
&:hover .svg-close {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.svg-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.orb-icon {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
margin: 0 24px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
margin-bottom: 12px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
margin-bottom: 24px;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg?raw';
|
||||
import starGroup from '@/assets/svg/star-group.svg?raw';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
starGroup,
|
||||
close: closeIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
|
||||
@@ -108,15 +108,15 @@ export default {
|
||||
const allEmails = [];
|
||||
if (user.auth.local.email) allEmails.push(user.auth.local.email);
|
||||
if (user.auth.google && user.auth.google.emails) {
|
||||
const emails = user.auth.google.emails;
|
||||
const { emails } = user.auth.google;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.apple && user.auth.apple.emails) {
|
||||
const emails = user.auth.apple.emails;
|
||||
const { emails } = user.auth.apple;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||
const emails = user.auth.facebook.emails;
|
||||
const { emails } = user.auth.facebook;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
return allEmails;
|
||||
|
||||
+1
-1
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
|
||||
@@ -20,6 +20,29 @@
|
||||
class="form mx-auto"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<div v-if="needsEmailField">
|
||||
<input
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailAddress')"
|
||||
:class="{
|
||||
'mb-3': !emailError,
|
||||
'input-invalid input-with-error mb-2': emailError,
|
||||
'input-valid': email && emailValid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
<p class="purple-600 mb-3">
|
||||
{{ $t('emailRequiredForSupport') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
@@ -58,8 +81,9 @@
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!username || usernameInvalid || !privacyAccepted"
|
||||
class="btn btn-info d-flex justify-content-center
|
||||
align-items-center w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
@@ -133,10 +157,12 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,23 +174,19 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [sanitizeRedirect],
|
||||
mixins: [accountCreation, sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
privacyAccepted: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
needsEmailField: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -183,30 +205,40 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (!this.email) {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
if (!this.email) {
|
||||
this.email = window.sessionStorage.getItem('apple-email');
|
||||
}
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
|
||||
if (!this.email && this.registrationMethod !== 'apple') {
|
||||
return;
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
|
||||
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
|
||||
this.needsEmailField = true;
|
||||
}
|
||||
if (this.email) {
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
}
|
||||
document.getElementById('usernameInput').focus();
|
||||
},
|
||||
methods: {
|
||||
@@ -237,6 +269,7 @@ export default {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -321,10 +321,11 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment()
|
||||
.isBetween(event.start, event.end));
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
const substitutionEvent = this.currentEventList?.find(event => event.spriteSubstitutions
|
||||
&& moment().isBetween(event.start, event.end));
|
||||
if (substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
|
||||
return this.foolPet(`Pet-${this.member.items.currentPet}`,
|
||||
substitutionEvent.spriteSubstitutions.pets);
|
||||
}
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
|
||||
@@ -12,23 +12,39 @@
|
||||
<label>
|
||||
<strong v-once>{{ $t('name') }} *</strong>
|
||||
</label>
|
||||
<b-form-input
|
||||
<input
|
||||
ref="nameInput"
|
||||
v-model="workingChallenge.name"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('challengeNamePlaceholder')"
|
||||
@keydown="enableSubmit"
|
||||
/>
|
||||
@focus="setActiveField('name')"
|
||||
@keydown="onFieldKeydown($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<strong v-once>{{ $t('shortName') }} *</strong>
|
||||
</label>
|
||||
<b-form-input
|
||||
<input
|
||||
ref="shortNameInput"
|
||||
v-model="workingChallenge.shortName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('shortNamePlaceholder')"
|
||||
@keydown="enableSubmit"
|
||||
/>
|
||||
@focus="setActiveField('shortName')"
|
||||
@keydown="onFieldKeydown($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
@@ -40,10 +56,17 @@
|
||||
{{ $t('charactersRemaining', {characters: charactersRemaining}) }}
|
||||
</div>
|
||||
<textarea
|
||||
ref="summaryTextarea"
|
||||
v-model="workingChallenge.summary"
|
||||
class="summary-textarea form-control"
|
||||
:placeholder="$t('challengeSummaryPlaceholder')"
|
||||
@keydown="enableSubmit"
|
||||
@focus="setActiveField('summary')"
|
||||
@keydown="onFieldKeydown($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -55,11 +78,26 @@
|
||||
class="float-right"
|
||||
></a>
|
||||
<textarea
|
||||
ref="descriptionTextarea"
|
||||
v-model="workingChallenge.description"
|
||||
class="description-textarea form-control"
|
||||
:placeholder="$t('challengeDescriptionPlaceholder')"
|
||||
@keydown="enableSubmit"
|
||||
@focus="setActiveField('description')"
|
||||
@keydown="onFieldKeydown($event)"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="activeFieldText"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="creating"
|
||||
@@ -280,12 +318,17 @@ import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHAL
|
||||
import CategoryOptions from '@/../../common/script/content/categoryOptions';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
emojiAutoComplete,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
mixins: [userStateMixin, autoCompleteHelperMixin],
|
||||
props: ['groupId'],
|
||||
data () {
|
||||
const categoryOptions = CategoryOptions;
|
||||
@@ -319,9 +362,14 @@ export default {
|
||||
categoriesHashByKey,
|
||||
loading: false,
|
||||
groups: [],
|
||||
textbox: null,
|
||||
activeField: 'name',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeFieldText () {
|
||||
return this.workingChallenge[this.activeField] || '';
|
||||
},
|
||||
creating () {
|
||||
return !this.workingChallenge.id;
|
||||
},
|
||||
@@ -589,6 +637,29 @@ export default {
|
||||
toggleCategorySelect () {
|
||||
this.showCategorySelect = !this.showCategorySelect;
|
||||
},
|
||||
setActiveField (field) {
|
||||
this.activeField = field;
|
||||
const refMap = {
|
||||
name: 'nameInput',
|
||||
shortName: 'shortNameInput',
|
||||
summary: 'summaryTextarea',
|
||||
description: 'descriptionTextarea',
|
||||
};
|
||||
this.textbox = this.$refs[refMap[field]] || null;
|
||||
},
|
||||
onFieldKeydown (e) {
|
||||
this.enableSubmit();
|
||||
this.autoCompleteMixinUpdateCarretPosition(e);
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.workingChallenge[this.activeField] = newText;
|
||||
this.$nextTick(() => {
|
||||
if (this.textbox) {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
enableSubmit: throttle(function enableSubmit () {
|
||||
/* Enables the submit button if it was disabled */
|
||||
if (this.loading) {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="searchResults.length > 0"
|
||||
class="autocomplete-selection"
|
||||
:style="autocompleteStyle"
|
||||
>
|
||||
<div
|
||||
v-for="result in searchResults"
|
||||
:key="result.shortcode"
|
||||
class="autocomplete-results d-flex align-items-center"
|
||||
:class="{'hover-background': result.hover}"
|
||||
@click="select(result)"
|
||||
@mouseenter="setHover(result)"
|
||||
@mouseleave="resetSelection()"
|
||||
>
|
||||
<img
|
||||
v-if="result.imageUrl"
|
||||
class="emoji-img"
|
||||
:src="result.imageUrl"
|
||||
:alt="result.shortcode"
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-char"
|
||||
>{{ result.emoji }}</span>
|
||||
<span
|
||||
class="shortcode ml-2"
|
||||
:class="{'hover-foreground': result.hover}"
|
||||
>:{{ result.shortcode }}:</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.autocomplete-results {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.autocomplete-selection {
|
||||
box-shadow: 1px 1px 1px #efefef;
|
||||
}
|
||||
|
||||
.hover-background {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hover-foreground {
|
||||
color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.emoji-char {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.emoji-img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.shortcode {
|
||||
color: $gray-200;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import habiticaMarkdown from 'habitica-markdown';
|
||||
|
||||
export default {
|
||||
props: ['text', 'caretPosition', 'coords', 'textbox'],
|
||||
data () {
|
||||
return {
|
||||
colonRegex: /:([a-zA-Z0-9_+]*)$/,
|
||||
currentSearch: '',
|
||||
searchActive: false,
|
||||
searchResults: [],
|
||||
selected: null,
|
||||
emojiList: [],
|
||||
renderTick: 0,
|
||||
internalCoords: { TOP: 0, LEFT: 0 },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
autocompleteStyle () {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _tick = this.renderTick;
|
||||
const isTextarea = this.textbox.tagName === 'TEXTAREA';
|
||||
const dropdownPA = (this.$el && this.$el.nodeType === 1) ? this.$el.offsetParent : null;
|
||||
const textboxOP = this.textbox.offsetParent;
|
||||
const needsRectCalc = dropdownPA && textboxOP && dropdownPA !== textboxOP;
|
||||
|
||||
let top;
|
||||
let left;
|
||||
const caretLeft = this.internalCoords.LEFT - (this.textbox.scrollLeft || 0);
|
||||
|
||||
if (needsRectCalc) {
|
||||
const textboxRect = this.textbox.getBoundingClientRect();
|
||||
const parentRect = dropdownPA.getBoundingClientRect();
|
||||
const parentScrollTop = dropdownPA.scrollTop || 0;
|
||||
|
||||
if (isTextarea) {
|
||||
const computedStyle = window.getComputedStyle(this.textbox);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight)
|
||||
|| (parseFloat(computedStyle.fontSize) * 1.4);
|
||||
const caretTopInTextbox = this.internalCoords.TOP
|
||||
- (this.textbox.scrollTop || 0) + lineHeight;
|
||||
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
|
||||
top = (textboxRect.top - parentRect.top) + parentScrollTop + clamped + 2;
|
||||
} else {
|
||||
top = (textboxRect.bottom - parentRect.top) + parentScrollTop + 2;
|
||||
}
|
||||
left = (textboxRect.left - parentRect.left) + caretLeft;
|
||||
} else {
|
||||
if (isTextarea) {
|
||||
const computedStyle = window.getComputedStyle(this.textbox);
|
||||
const lineHeight = parseFloat(computedStyle.lineHeight)
|
||||
|| (parseFloat(computedStyle.fontSize) * 1.4);
|
||||
const caretTopInTextbox = this.internalCoords.TOP
|
||||
- (this.textbox.scrollTop || 0) + lineHeight;
|
||||
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
|
||||
top = this.textbox.offsetTop + clamped + 2;
|
||||
} else {
|
||||
top = this.textbox.offsetTop + this.textbox.offsetHeight + 2;
|
||||
}
|
||||
left = this.textbox.offsetLeft + caretLeft;
|
||||
}
|
||||
|
||||
return {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
position: 'absolute',
|
||||
minWidth: '150px',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'white',
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchResults (results, oldResults) {
|
||||
if (results.length > 0 && (!oldResults || oldResults.length === 0)) {
|
||||
this.$nextTick(() => {
|
||||
this.renderTick += 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
text (newText, prevText) {
|
||||
if (!this.textbox) return;
|
||||
this._measureCaretCoords();
|
||||
const delCharsBool = prevText.length > newText.length;
|
||||
const caretPosition = this.textbox.selectionEnd;
|
||||
const lastFocusChar = delCharsBool ? prevText[caretPosition] : newText[caretPosition - 1];
|
||||
if (
|
||||
newText.length === 0
|
||||
|| (lastFocusChar === ':' && delCharsBool)
|
||||
) {
|
||||
this.cancel();
|
||||
} else {
|
||||
if (lastFocusChar === ':') this.searchActive = true;
|
||||
if (this.searchActive) {
|
||||
this.searchResults = this.solveSearchResults(newText.substring(0, caretPosition));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
created () {
|
||||
const defs = habiticaMarkdown.emojiDefs;
|
||||
if (!defs) return;
|
||||
const customEmojis = habiticaMarkdown.customEmojis || {};
|
||||
const list = [];
|
||||
const keys = Object.keys(defs);
|
||||
keys.sort();
|
||||
for (const key of keys) {
|
||||
const entry = { shortcode: key, emoji: defs[key], hover: false };
|
||||
if (customEmojis[key]) {
|
||||
entry.imageUrl = customEmojis[key];
|
||||
}
|
||||
list.push(entry);
|
||||
}
|
||||
this.emojiList = list;
|
||||
},
|
||||
methods: {
|
||||
solveSearchResults (textFocus) {
|
||||
const regexRes = this.colonRegex.exec(textFocus);
|
||||
if (!regexRes) {
|
||||
this.cancel();
|
||||
return [];
|
||||
}
|
||||
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
if (this.currentSearch.length === 0) return [];
|
||||
|
||||
const lowerSearch = this.currentSearch.toLowerCase();
|
||||
return this.emojiList
|
||||
.filter(entry => entry.shortcode.startsWith(lowerSearch))
|
||||
.slice(0, 6)
|
||||
.map(entry => ({ ...entry, hover: false }));
|
||||
},
|
||||
select (result) {
|
||||
const { text } = this;
|
||||
const targetName = `${result.shortcode}: `;
|
||||
const oldCaret = this.caretPosition;
|
||||
const escapedSearch = this.currentSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
let newText = text.substring(0, this.caretPosition)
|
||||
.replace(new RegExp(`${escapedSearch}$`), targetName);
|
||||
const newCaret = newText.length;
|
||||
newText += text.substring(oldCaret, text.length);
|
||||
this.$emit('select', newText, newCaret);
|
||||
|
||||
this.cancel();
|
||||
},
|
||||
setHover (result) {
|
||||
this.resetSelection();
|
||||
result.hover = true;
|
||||
},
|
||||
clearHover () {
|
||||
for (const selection of this.searchResults) {
|
||||
selection.hover = false;
|
||||
}
|
||||
},
|
||||
resetSelection () {
|
||||
this.clearHover();
|
||||
this.selected = null;
|
||||
},
|
||||
selectNext () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null
|
||||
? 0
|
||||
: (this.selected + 1) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
selectPrevious () {
|
||||
if (this.searchResults.length > 0) {
|
||||
this.clearHover();
|
||||
this.selected = this.selected === null
|
||||
? this.searchResults.length - 1
|
||||
: (this.selected - 1 + this.searchResults.length) % this.searchResults.length;
|
||||
this.searchResults[this.selected].hover = true;
|
||||
}
|
||||
},
|
||||
makeSelection () {
|
||||
if (this.searchResults.length > 0 && this.selected !== null) {
|
||||
const result = this.searchResults[this.selected];
|
||||
this.select(result);
|
||||
}
|
||||
},
|
||||
_measureCaretCoords () {
|
||||
const el = this.textbox;
|
||||
const caretPosition = el.selectionEnd;
|
||||
const div = document.createElement('div');
|
||||
const span = document.createElement('span');
|
||||
const copyStyle = getComputedStyle(el);
|
||||
|
||||
[].forEach.call(copyStyle, prop => {
|
||||
div.style[prop] = copyStyle[prop];
|
||||
});
|
||||
|
||||
div.style.position = 'absolute';
|
||||
div.style.visibility = 'hidden';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = el.value.substr(0, caretPosition);
|
||||
span.textContent = el.value.substr(caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.internalCoords = {
|
||||
TOP: span.offsetTop,
|
||||
LEFT: span.offsetLeft,
|
||||
};
|
||||
document.body.removeChild(div);
|
||||
},
|
||||
cancel () {
|
||||
this.searchActive = false;
|
||||
this.searchResults = [];
|
||||
this.resetSelection();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -187,7 +187,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.purchased.background.birthday_bash"
|
||||
v-if="user.purchased.background.birthday_bash
|
||||
|| user.purchased.background.on_a_strange_planet"
|
||||
>
|
||||
<div
|
||||
class="row justify-content-center title-row mb-3"
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="group-plan-selection"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
size="md"
|
||||
@show="loadData"
|
||||
@hide="onHide"
|
||||
>
|
||||
<div class="selection-modal">
|
||||
<div class="modal-header-row">
|
||||
<h2 class="title">
|
||||
{{ $t('chooseAnOption') }}
|
||||
</h2>
|
||||
<div class="header-actions">
|
||||
<span
|
||||
class="cancel-text"
|
||||
@click="close"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-primary next-button"
|
||||
:class="{ disabled: !selectedOption }"
|
||||
:disabled="!selectedOption"
|
||||
@click="continueFlow"
|
||||
>
|
||||
{{ $t('next') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading-container"
|
||||
>
|
||||
<div class="spinner-border text-secondary"></div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-if="hasUpgradeableGroups"
|
||||
class="section-header"
|
||||
>
|
||||
{{ $t('upgradeExistingGroup') }}
|
||||
</div>
|
||||
|
||||
<selectable-card
|
||||
v-for="group in upgradeableGuilds"
|
||||
:key="group._id"
|
||||
class="option-card"
|
||||
:selected="isSelected(group)"
|
||||
@click="selectOption(group)"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-info">
|
||||
<div class="option-name">
|
||||
{{ group.name }}
|
||||
</div>
|
||||
<div class="option-members">
|
||||
{{ formatMemberCount(group.memberCount) }}
|
||||
</div>
|
||||
<div class="option-label previously-upgraded">
|
||||
<div
|
||||
class="svg-icon sparkle-icon"
|
||||
v-html="icons.sparkles"
|
||||
></div>
|
||||
{{ $t('previouslyUpgradedGroup') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-price">
|
||||
${{ calculatePrice(group.memberCount) }}.00/mo
|
||||
</div>
|
||||
</div>
|
||||
</selectable-card>
|
||||
|
||||
<selectable-card
|
||||
v-if="upgradeableParty"
|
||||
class="option-card"
|
||||
:class="{ 'has-pending-warning': partyPendingInviteCount > 0 }"
|
||||
:selected="isSelected(upgradeableParty)"
|
||||
@click="selectOption(upgradeableParty)"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-info">
|
||||
<div class="option-name">
|
||||
{{ upgradeableParty.name }}
|
||||
</div>
|
||||
<div class="option-members">
|
||||
{{ formatMemberCount(upgradeableParty.memberCount) }}
|
||||
<span
|
||||
v-if="partyPendingInviteCount > 0"
|
||||
class="pending-count"
|
||||
>
|
||||
{{ $t('pendingCount', { count: partyPendingInviteCount }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isPartyPreviouslyUpgraded"
|
||||
class="option-label previously-upgraded"
|
||||
>
|
||||
<div
|
||||
class="svg-icon sparkle-icon"
|
||||
v-html="icons.sparkles"
|
||||
></div>
|
||||
{{ $t('previouslyUpgradedGroup') }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="option-label your-party"
|
||||
>
|
||||
<div
|
||||
class="svg-icon member-icon"
|
||||
v-html="icons.member"
|
||||
></div>
|
||||
{{ $t('yourParty') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-price">
|
||||
${{ calculatePrice(upgradeableParty.memberCount) }}.00/mo
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="partyPendingInviteCount > 0"
|
||||
class="pending-warning-banner"
|
||||
>
|
||||
<div
|
||||
class="svg-icon alert-icon"
|
||||
v-html="icons.alert"
|
||||
></div>
|
||||
<span class="warning-text">{{ $t('upgradeCancelsPendingInvites') }}</span>
|
||||
</div>
|
||||
</selectable-card>
|
||||
|
||||
<div
|
||||
v-if="hasUpgradeableGroups"
|
||||
class="or-divider"
|
||||
>
|
||||
<div class="divider-line"></div>
|
||||
<span class="or-text">{{ $t('or') }}</span>
|
||||
<div class="divider-line"></div>
|
||||
</div>
|
||||
|
||||
<selectable-card
|
||||
class="option-card create-new"
|
||||
:selected="selectedOption === 'new'"
|
||||
@click="selectOption('new')"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="option-info">
|
||||
<div class="option-name">
|
||||
{{ $t('createNewGroup') }}
|
||||
</div>
|
||||
<div class="option-description">
|
||||
{{ $t('inviteOthersForAdditional') }}
|
||||
<span class="price-highlight">${{ perMemberPrice }}.00</span>
|
||||
{{ $t('perMember') }}.
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-price">
|
||||
${{ basePrice }}.00/mo
|
||||
</div>
|
||||
</div>
|
||||
</selectable-card>
|
||||
|
||||
<div class="footer-note">
|
||||
{{ $t('additionalMembersProrated') }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.selection-modal {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: $purple-200;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cancel-text {
|
||||
color: $blue-10;
|
||||
font-size: 0.875rem;
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
min-width: 64px;
|
||||
|
||||
&.disabled {
|
||||
background-color: $gray-300;
|
||||
border-color: $gray-300;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
color: $gray-10;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
margin-bottom: 12px;
|
||||
|
||||
::v-deep .option-name {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
&.selected ::v-deep .option-name {
|
||||
color: $purple-200;
|
||||
}
|
||||
}
|
||||
|
||||
.pending-warning-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
background-color: $yellow-50;
|
||||
border-radius: 0 0 6px 6px;
|
||||
margin: 16px -16px 0 -16px;
|
||||
gap: 4px;
|
||||
|
||||
.selected & {
|
||||
margin: 15px -15px 0 -15px;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
::v-deep path {
|
||||
fill: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-text {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.option-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding-left: 32px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.option-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option-name {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.option-members {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-100;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.pending-count {
|
||||
font-weight: 700;
|
||||
color: $yellow-5;
|
||||
}
|
||||
}
|
||||
|
||||
.option-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
gap: 4px;
|
||||
|
||||
&.previously-upgraded {
|
||||
font-weight: 700;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
&.your-party {
|
||||
font-weight: 700;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.sparkle-icon {
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.member-icon {
|
||||
color: $gray-100;
|
||||
|
||||
::v-deep path {
|
||||
fill: $gray-100;
|
||||
stroke: $gray-100;
|
||||
stroke-width: 0.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-description {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-100;
|
||||
|
||||
.price-highlight {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.option-price {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: $purple-200;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.or-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
|
||||
.divider-line {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background-color: $gray-500;
|
||||
}
|
||||
|
||||
.or-text {
|
||||
padding: 0 16px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
.create-new {
|
||||
.option-name {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-100;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
margin-left: 24px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
#group-plan-selection {
|
||||
.modal-dialog {
|
||||
max-width: 504px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.option-card.has-pending-warning.selectable-card {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import SelectableCard from '@/components/ui/selectableCard.vue';
|
||||
import svgSparkles from '@/assets/svg/sparkles.svg?raw';
|
||||
import svgMember from '@/assets/svg/member-icon.svg?raw';
|
||||
import svgAlert from '@/assets/svg/for-css/alert.svg?raw';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SelectableCard,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
selectedOption: null,
|
||||
userGuilds: [],
|
||||
userParty: null,
|
||||
activeGroupPlanIds: [],
|
||||
loading: true,
|
||||
basePrice: 9,
|
||||
perMemberPrice: 3,
|
||||
icons: Object.freeze({
|
||||
sparkles: svgSparkles,
|
||||
member: svgMember,
|
||||
alert: svgAlert,
|
||||
}),
|
||||
partyPendingInviteCount: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
upgradeableGuilds () {
|
||||
return this.userGuilds.filter(group => {
|
||||
const leaderId = group.leader?._id || group.leader;
|
||||
if (leaderId !== this.user._id) return false;
|
||||
const { purchased } = group;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (this.activeGroupPlanIds.includes(group._id)) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
return new Date(purchased.dateTerminated) < new Date();
|
||||
});
|
||||
},
|
||||
upgradeableParty () {
|
||||
if (!this.userParty) return null;
|
||||
|
||||
const leaderId = this.userParty.leader?._id || this.userParty.leader;
|
||||
if (leaderId !== this.user._id) return null;
|
||||
|
||||
if (this.activeGroupPlanIds.includes(this.userParty._id)) return null;
|
||||
|
||||
return this.userParty;
|
||||
},
|
||||
hasUpgradeableGroups () {
|
||||
return this.upgradeableGuilds.length > 0 || this.upgradeableParty !== null;
|
||||
},
|
||||
isPartyPreviouslyUpgraded () {
|
||||
if (!this.userParty) return false;
|
||||
const { purchased } = this.userParty;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
return new Date(purchased.dateTerminated) < new Date();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async loadData () {
|
||||
this.loading = true;
|
||||
this.selectedOption = null;
|
||||
this.partyPendingInviteCount = 0;
|
||||
|
||||
try {
|
||||
const [guildsResponse, partyResponse] = await Promise.all([
|
||||
axios.get('/api/v4/groups', { params: { type: 'guilds', includeExpiredPlans: 'true' } }),
|
||||
axios.get('/api/v4/groups/party').catch(() => ({ data: { data: null } })),
|
||||
]);
|
||||
|
||||
this.userGuilds = guildsResponse.data.data || [];
|
||||
this.userParty = partyResponse.data.data;
|
||||
|
||||
if (this.userParty) {
|
||||
try {
|
||||
const invitesResponse = await axios.get(`/api/v4/groups/${this.userParty._id}/invites`);
|
||||
this.partyPendingInviteCount = invitesResponse.data.data?.length || 0;
|
||||
} catch (e) {
|
||||
this.partyPendingInviteCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:getGroupPlans', true);
|
||||
const groupPlans = this.$store.state.groupPlans?.data || [];
|
||||
this.activeGroupPlanIds = groupPlans.map(g => g._id);
|
||||
} catch (e) {
|
||||
console.error('Error loading group data:', e);
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.upgradeableGuilds.length > 0) {
|
||||
[this.selectedOption] = this.upgradeableGuilds;
|
||||
} else if (this.upgradeableParty) {
|
||||
this.selectedOption = this.upgradeableParty;
|
||||
} else {
|
||||
this.selectedOption = 'new';
|
||||
}
|
||||
});
|
||||
},
|
||||
selectOption (option) {
|
||||
this.selectedOption = option;
|
||||
},
|
||||
isSelected (group) {
|
||||
if (!this.selectedOption || this.selectedOption === 'new') return false;
|
||||
return this.selectedOption._id === group._id;
|
||||
},
|
||||
calculatePrice (memberCount) {
|
||||
return this.basePrice + (this.perMemberPrice * (memberCount - 1));
|
||||
},
|
||||
formatMemberCount (count) {
|
||||
return count === 1 ? this.$t('oneMember') : this.$t('membersCount', { count });
|
||||
},
|
||||
continueFlow () {
|
||||
if (!this.selectedOption) return;
|
||||
|
||||
const selection = this.selectedOption;
|
||||
this.close();
|
||||
|
||||
if (selection === 'new') {
|
||||
this.$root.$emit('bv::show::modal', 'create-group');
|
||||
} else {
|
||||
this.stripeGroup({ group: selection, upgrade: true });
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'group-plan-selection');
|
||||
},
|
||||
onHide () {
|
||||
this.selectedOption = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -41,6 +41,14 @@
|
||||
:chat="group.chat"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
<community-guidelines />
|
||||
<div class="row chat-actions">
|
||||
@@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
|
||||
import autocomplete from '../chat/autoComplete';
|
||||
import emojiAutoComplete from '../chat/emojiAutoComplete';
|
||||
import communityGuidelines from './communityGuidelines';
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import { mapState } from '@/libs/store';
|
||||
@@ -102,6 +111,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
autocomplete,
|
||||
emojiAutoComplete,
|
||||
communityGuidelines,
|
||||
chatMessages,
|
||||
},
|
||||
|
||||
@@ -25,53 +25,61 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="row icon-row">
|
||||
<div
|
||||
class="item-with-icon"
|
||||
class="item-with-icon p-2"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keyup.enter="showMemberModal()"
|
||||
@click="showMemberModal()"
|
||||
>
|
||||
<div
|
||||
v-if="group.memberCount > 1000"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.goldGuildBadgeIcon"
|
||||
></div>
|
||||
<div
|
||||
v-if="group.memberCount > 100 && group.memberCount < 999"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.silverGuildBadgeIcon"
|
||||
></div>
|
||||
<div
|
||||
v-if="group.memberCount < 100"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.bronzeGuildBadgeIcon"
|
||||
></div>
|
||||
<span class="number">{{ group.memberCount | abbrNum }}</span>
|
||||
<div
|
||||
v-once
|
||||
class="member-list label"
|
||||
>
|
||||
{{ $t('memberList') }}
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
v-if="group.memberCount > 1000"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.goldGuildBadgeIcon"
|
||||
></div>
|
||||
<div
|
||||
v-if="group.memberCount > 100 && group.memberCount < 999"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.silverGuildBadgeIcon"
|
||||
></div>
|
||||
<div
|
||||
v-if="group.memberCount < 100"
|
||||
class="svg-icon shield"
|
||||
v-html="icons.bronzeGuildBadgeIcon"
|
||||
></div>
|
||||
<span class="number">{{ group.memberCount | abbrNum }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('memberList') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isParty">
|
||||
<div
|
||||
class="item-with-icon"
|
||||
class="item-with-icon p-2"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
@keyup.enter="showGroupGems()"
|
||||
@click="showGroupGems()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="number">{{ group.balance * 4 }}</span>
|
||||
<div
|
||||
v-once
|
||||
class="label"
|
||||
>
|
||||
{{ $t('guildBank') }}
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
class="svg-icon gem"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="number">{{ group.balance * 4 }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('guildBank') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,35 +136,57 @@
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
background-color: #ffffff;
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
min-width: 120px;
|
||||
margin-left: 1em;
|
||||
width: 120px;
|
||||
height: 76px;
|
||||
margin-right: 1rem;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&:last-of-type {
|
||||
margin-left: 0.5rem;
|
||||
.box-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.svg-icon.shield, .svg-icon.gem {
|
||||
width: 28px;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
.icon-number-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.1em;
|
||||
|
||||
.number {
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-top: .5em;
|
||||
.details {
|
||||
font-size: 11px;
|
||||
color: $gray-200;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
line-height: 1.1;
|
||||
word-break: break-word;
|
||||
max-height: 2.2em;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,11 +245,6 @@
|
||||
.icon-row {
|
||||
margin-top: 1em;
|
||||
justify-content: flex-end;
|
||||
|
||||
.number {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-row {
|
||||
|
||||
@@ -182,12 +182,10 @@ export default {
|
||||
return 'GreyedOut';
|
||||
},
|
||||
imageName () {
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (this.isOwned() && foolEvent) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
|
||||
const substitutionEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.spriteSubstitutions);
|
||||
if (this.isOwned() && substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
|
||||
return `stable_${this.foolPet(`Pet-${this.item.key}`, substitutionEvent.spriteSubstitutions.pets)}`;
|
||||
}
|
||||
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
>
|
||||
<div class="modal-body">
|
||||
<news-content ref="newsContent" />
|
||||
<close-x
|
||||
@close="dismissAlert()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer d-flex align-items-center pb-0">
|
||||
@@ -30,12 +33,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import newsContent from './newsContent';
|
||||
import closeX from '../ui/closeX.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
closeX,
|
||||
newsContent,
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
async onShow () {
|
||||
this.$refs.newsContent.getPosts();
|
||||
|
||||
@@ -330,6 +330,7 @@ export default {
|
||||
handledNotifications,
|
||||
isInitialLoadComplete: false,
|
||||
pendingRebirthNotification: null,
|
||||
lastShownStreakCount: null, // Track last shown streak to prevent duplicates
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -726,17 +727,24 @@ export default {
|
||||
this.$root.$emit('habitica:won-challenge', notification);
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
if (localStorage.getItem('show-rebirth-confirmation') === 'true') {
|
||||
markAsRead = false;
|
||||
} else if (!this.isInitialLoadComplete) {
|
||||
this.pendingRebirthNotification = notification;
|
||||
markAsRead = false;
|
||||
} else {
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
if (localStorage.getItem('show-rebirth-confirmation') !== 'true') {
|
||||
if (!this.isInitialLoadComplete) {
|
||||
this.pendingRebirthNotification = notification;
|
||||
markAsRead = false;
|
||||
} else {
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'STREAK_ACHIEVEMENT':
|
||||
// Client-side deduplication: prevent showing duplicate streak achievements
|
||||
if (this.lastShownStreakCount === this.user.achievements.streak) {
|
||||
// Same streak already shown, skip this notification
|
||||
break;
|
||||
}
|
||||
this.lastShownStreakCount = this.user.achievements.streak;
|
||||
|
||||
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
|
||||
this.$root.$emit('bv::show::modal', 'streak');
|
||||
}, this.user.preferences.suppressModals.streak);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
:hide-class-badge="true"
|
||||
:with-background="true"
|
||||
:override-avatar-gear="getAvatarOverrides(item)"
|
||||
:sprites-margin="'0px auto 0px -24px'"
|
||||
:sprites-margin="'0px auto 0px -2px'"
|
||||
/>
|
||||
</div>
|
||||
<item
|
||||
|
||||
@@ -498,8 +498,13 @@ export default {
|
||||
|
||||
await this.triggerGetWorldState();
|
||||
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
|
||||
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
|
||||
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
|
||||
if (this.currentEvent.season === 'valentines') {
|
||||
this.imageURLs.background = 'url(/static/npc/spring/seasonal_shop_opened_background.png)';
|
||||
this.imageURLs.npc = 'url(/static/npc/spring/seasonal_shop_opened_npc.png)';
|
||||
} else {
|
||||
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
|
||||
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -37,6 +37,9 @@ export default {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
if (response.email) {
|
||||
window.sessionStorage.setItem('apple-email', response.email);
|
||||
}
|
||||
window.location.href = '/username';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<group-plan-selection-modal />
|
||||
<group-plan-creation-modal />
|
||||
<div class="d-flex justify-content-center">
|
||||
<div
|
||||
@@ -315,10 +316,12 @@
|
||||
import { setup as setupPayments } from '@/libs/payments';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import GroupPlanCreationModal from '../group-plans/groupPlanCreationModal.vue';
|
||||
import GroupPlanSelectionModal from '../group-plans/groupPlanSelectionModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GroupPlanCreationModal,
|
||||
GroupPlanSelectionModal,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
@@ -359,7 +362,7 @@ export default {
|
||||
if (this.upgradingGroup._id) {
|
||||
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });
|
||||
}
|
||||
return this.$root.$emit('bv::show::modal', 'create-group');
|
||||
return this.$root.$emit('bv::show::modal', 'group-plan-selection');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -348,7 +348,6 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import draggable from 'vuedraggable';
|
||||
import { shouldDo } from '@/../../common/script/cron';
|
||||
import inAppRewards from '@/../../common/script/libs/inAppRewards';
|
||||
import taskDefaults from '@/../../common/script/libs/taskDefaults';
|
||||
import Task from './task';
|
||||
@@ -482,25 +481,10 @@ export default {
|
||||
return this.$t('addATask', { type });
|
||||
},
|
||||
badgeCount () {
|
||||
// 0 means the badge will not be shown
|
||||
// It is shown for the all and due views of dailies
|
||||
// and for the active and scheduled views of todos.
|
||||
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
|
||||
return this.taskList.length;
|
||||
} if (this.type === 'daily') {
|
||||
if (this.activeFilter.label === 'due') {
|
||||
return this.taskList.length;
|
||||
} if (this.activeFilter.label === 'all') {
|
||||
return this.taskList
|
||||
.reduce(
|
||||
(count, t) => (!t.completed
|
||||
&& shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count),
|
||||
0,
|
||||
);
|
||||
}
|
||||
if (this.type === 'reward') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return this.taskList.length;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -48,11 +48,19 @@
|
||||
/>
|
||||
|
||||
<input
|
||||
:ref="'checklistItem-' + $index"
|
||||
v-model="item.text"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:disabled="disabled || disableEdit"
|
||||
:class="summaryClass(item)"
|
||||
@focus="setActiveItem($index)"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
<span
|
||||
v-if="!disabled && !disableEdit"
|
||||
@@ -81,15 +89,30 @@
|
||||
</span>
|
||||
|
||||
<input
|
||||
ref="newChecklistInput"
|
||||
v-model="newChecklistItem"
|
||||
class="inline-edit-input checklist-item form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newChecklistItem')"
|
||||
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
|
||||
@focus="setActiveItem(-1)"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="newChecklistEnterHandler($event)"
|
||||
@keyup.enter="addChecklistItem($event, true)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
@blur="addChecklistItem($event, false)"
|
||||
>
|
||||
</div>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="activeFieldText"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</template>
|
||||
@@ -105,6 +128,8 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
|
||||
import gripIcon from '@/assets/svg/grip.svg?raw';
|
||||
import checkbox from '@/components/ui/checkbox';
|
||||
import lockableLabel from './lockableLabel';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
name: 'Checklist',
|
||||
@@ -112,7 +137,9 @@ export default {
|
||||
checkbox,
|
||||
draggable,
|
||||
lockableLabel,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
mixins: [autoCompleteHelperMixin],
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -133,6 +160,8 @@ export default {
|
||||
showChecklist: true,
|
||||
hasPossibilityOfIMEConversion: true,
|
||||
newChecklistItem: null,
|
||||
textbox: null,
|
||||
activeItemIndex: -1,
|
||||
icons: Object.freeze({
|
||||
positive: positiveIcon,
|
||||
destroy: deleteIcon,
|
||||
@@ -141,6 +170,15 @@ export default {
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
activeFieldText () {
|
||||
if (this.activeItemIndex === -1) {
|
||||
return this.newChecklistItem || '';
|
||||
}
|
||||
const item = this.checklist[this.activeItemIndex];
|
||||
return item ? item.text || '' : '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
summaryClass (item) {
|
||||
if (!this.disableEdit) return '';
|
||||
@@ -179,6 +217,40 @@ export default {
|
||||
this.checklist.splice(i, 1);
|
||||
this.updateChecklist();
|
||||
},
|
||||
setActiveItem (index) {
|
||||
this.activeItemIndex = index;
|
||||
if (index === -1) {
|
||||
this.textbox = this.$refs.newChecklistInput;
|
||||
} else {
|
||||
const refArr = this.$refs[`checklistItem-${index}`];
|
||||
this.textbox = refArr ? refArr[0] || refArr : null;
|
||||
}
|
||||
},
|
||||
newChecklistEnterHandler (e) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
ac.makeSelection();
|
||||
} else if (ac) {
|
||||
ac.cancel();
|
||||
this.setHasPossibilityOfIMEConversion(false);
|
||||
} else {
|
||||
this.setHasPossibilityOfIMEConversion(false);
|
||||
}
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
if (this.activeItemIndex === -1) {
|
||||
this.newChecklistItem = newText;
|
||||
} else {
|
||||
this.checklist[this.activeItemIndex].text = newText;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (this.textbox) {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -187,6 +259,7 @@ export default {
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.checklist-component {
|
||||
position: relative;
|
||||
|
||||
.chevron-flip {
|
||||
transform: translateY(-5px) rotate(180deg);
|
||||
|
||||
@@ -9,12 +9,27 @@
|
||||
@toggle="openOrClose($event)"
|
||||
>
|
||||
<b-dropdown-header>
|
||||
<div class="mb-2">
|
||||
<div class="mb-2 search-input-wrapper">
|
||||
<b-form-input
|
||||
ref="searchInput"
|
||||
v-model="search"
|
||||
type="text"
|
||||
:placeholder="searchPlaceholder"
|
||||
@keyup.enter="handleSubmit"
|
||||
@focus="setTextbox"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keydown.enter="searchEnterHandler($event)"
|
||||
@keydown.esc="searchEscHandler($event)"
|
||||
/>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="search"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +56,7 @@
|
||||
v-if="addNew || availableToSelect.length > 0"
|
||||
:class="{
|
||||
'item-group': true,
|
||||
'add-new': availableToSelect.length === 0 && search !== '',
|
||||
'add-new': search !== '' && !hasExactMatch,
|
||||
'scroll': availableToSelect.length > 5
|
||||
}"
|
||||
>
|
||||
@@ -71,7 +86,7 @@
|
||||
</b-dropdown-item-button>
|
||||
|
||||
<div
|
||||
v-if="addNew"
|
||||
v-if="addNew && search !== '' && !hasExactMatch"
|
||||
class="hint"
|
||||
>
|
||||
{{ $t('pressEnterToAddTag', { tagName: search }) }}
|
||||
@@ -94,6 +109,10 @@ $itemHeight: 2rem;
|
||||
}
|
||||
|
||||
.select-multi {
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
@@ -152,7 +171,8 @@ $itemHeight: 2rem;
|
||||
max-height: #{5*$itemHeight};
|
||||
|
||||
&.add-new {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
height: auto;
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
@@ -185,6 +205,8 @@ $itemHeight: 2rem;
|
||||
import Vue from 'vue';
|
||||
import MultiList from '@/components/tasks/modal-controls/multiList';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
@@ -192,7 +214,9 @@ export default {
|
||||
},
|
||||
components: {
|
||||
MultiList,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
mixins: [autoCompleteHelperMixin],
|
||||
props: {
|
||||
addNew: {
|
||||
type: Boolean,
|
||||
@@ -221,6 +245,8 @@ export default {
|
||||
wasTagAdded: false,
|
||||
selected: this.selectedItems,
|
||||
search: '',
|
||||
textbox: null,
|
||||
itemsAdded: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -248,6 +274,16 @@ export default {
|
||||
|
||||
return filteredItems;
|
||||
},
|
||||
hasExactMatch () {
|
||||
const searchTerm = this.search.trim().toLowerCase();
|
||||
if (!searchTerm) return false;
|
||||
if (this.itemsAdded.indexOf(searchTerm) !== -1) return true;
|
||||
if (this.availableToSelect.length === 0) return false;
|
||||
if (this.availableToSelect[0].name.toLowerCase() === searchTerm) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected () {
|
||||
@@ -286,6 +322,7 @@ export default {
|
||||
this.closeSelectPopup();
|
||||
},
|
||||
selectItem (item) {
|
||||
if (!item) return;
|
||||
this.selectedItems.push(item.id);
|
||||
this.$emit('toggle', item.id);
|
||||
this.preventHide = true;
|
||||
@@ -312,12 +349,51 @@ export default {
|
||||
this.closeSelectPopup();
|
||||
}
|
||||
},
|
||||
setTextbox () {
|
||||
const ref = this.$refs.searchInput;
|
||||
this.textbox = ref ? (ref.$el || ref) : null;
|
||||
},
|
||||
searchEnterHandler (e) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ac.makeSelection();
|
||||
} else {
|
||||
if (ac) ac.cancel();
|
||||
this.handleSubmit();
|
||||
}
|
||||
},
|
||||
searchEscHandler (e) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.searchActive) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
ac.cancel();
|
||||
}
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.search = newText;
|
||||
this.$nextTick(() => {
|
||||
if (this.textbox) {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSubmit () {
|
||||
if (!this.addNew) return;
|
||||
const { search } = this;
|
||||
this.$emit('addNew', search);
|
||||
|
||||
this.search = '';
|
||||
// If there is a existing tag
|
||||
if (this.hasExactMatch) {
|
||||
this.selectItem(this.availableToSelect[0]);
|
||||
this.search = '';
|
||||
} else {
|
||||
// Creating a new tag as there is no existing tag present
|
||||
this.$emit('addNew', search);
|
||||
this.itemsAdded.push(search.toLowerCase());
|
||||
this.search = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
spellcheck="true"
|
||||
:disabled="challengeAccessRequired"
|
||||
:placeholder="$t('addATitle')"
|
||||
@focus="setActiveField('title')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="titleEnterHandler($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@@ -92,11 +99,27 @@
|
||||
</small>
|
||||
</div>
|
||||
<textarea
|
||||
ref="notesTextarea"
|
||||
v-model="task.notes"
|
||||
class="form-control input-notes"
|
||||
:class="cssClass('input')"
|
||||
:placeholder="$t('addNotes')"
|
||||
@focus="setActiveField('notes')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="activeFieldText"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -712,6 +735,7 @@
|
||||
}
|
||||
|
||||
.task-modal-header {
|
||||
position: relative;
|
||||
color: $white;
|
||||
width: 100%;
|
||||
border-top-left-radius: 8px;
|
||||
@@ -1160,6 +1184,8 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import selectList from '@/components/ui/selectList';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
import negativeIcon from '@/assets/svg/negative.svg?raw';
|
||||
@@ -1182,15 +1208,18 @@ export default {
|
||||
toggleCheckbox,
|
||||
lockableLabel,
|
||||
selectList,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [syncTask],
|
||||
mixins: [syncTask, autoCompleteHelperMixin],
|
||||
// purpose is either create or edit, task is the task created or edited
|
||||
props: ['task', 'purpose', 'challengeId', 'groupId'],
|
||||
data () {
|
||||
return {
|
||||
textbox: null,
|
||||
activeField: 'title',
|
||||
showAssignedSelect: false,
|
||||
newChecklistItem: null,
|
||||
icons: Object.freeze({
|
||||
@@ -1314,6 +1343,10 @@ export default {
|
||||
selectedTags () {
|
||||
return this.getTagsFor(this.task);
|
||||
},
|
||||
activeFieldText () {
|
||||
if (!this.task) return '';
|
||||
return this.activeField === 'title' ? (this.task.text || '') : (this.task.notes || '');
|
||||
},
|
||||
showStatAssignment () {
|
||||
return this.task.type !== 'reward'
|
||||
&& !this.groupId
|
||||
@@ -1366,7 +1399,7 @@ export default {
|
||||
this.task.down = !this.task.down;
|
||||
},
|
||||
weekdaysMin (dayNumber) {
|
||||
return moment.weekdaysMin(dayNumber);
|
||||
return this.$t(`weekdaysMin${dayNumber}`);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
@@ -1489,6 +1522,35 @@ export default {
|
||||
},
|
||||
focusInput () {
|
||||
this.$refs.inputToFocus.focus();
|
||||
this.setActiveField('title');
|
||||
},
|
||||
setActiveField (field) {
|
||||
this.activeField = field;
|
||||
if (field === 'title') {
|
||||
this.textbox = this.$refs.inputToFocus;
|
||||
} else {
|
||||
this.textbox = this.$refs.notesTextarea;
|
||||
}
|
||||
},
|
||||
titleEnterHandler (e) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
ac.makeSelection();
|
||||
} else if (ac) {
|
||||
ac.cancel();
|
||||
}
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
if (this.activeField === 'title') {
|
||||
this.task.text = newText;
|
||||
} else {
|
||||
this.task.notes = newText;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
async addTag (name) {
|
||||
const tagResult = await this.createTag({ name });
|
||||
|
||||
@@ -80,9 +80,17 @@
|
||||
v-html="icons.drag"
|
||||
></div>
|
||||
<input
|
||||
:ref="'tagInput-' + tagIndex"
|
||||
v-model="tag.name"
|
||||
class="tag-edit-input inline-edit-input form-control"
|
||||
type="text"
|
||||
@focus="setActiveTag(tagIndex)"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
<div
|
||||
class="input-group-append"
|
||||
@@ -100,11 +108,18 @@
|
||||
class="col-6 dragSpace"
|
||||
>
|
||||
<input
|
||||
ref="newTagInput"
|
||||
v-model="newTag"
|
||||
class="new-tag-item edit-tag-item inline-edit-input form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newTag')"
|
||||
@keydown.enter="addTag($event, tagsType.key)"
|
||||
@focus="setActiveTag(-1)"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="newTagEnterHandler($event, tagsType.key)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</div>
|
||||
</draggable>
|
||||
@@ -134,6 +149,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<emoji-auto-complete
|
||||
v-if="editingTags"
|
||||
ref="emojiAutocomplete"
|
||||
:text="activeTagText"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedTagAutocomplete"
|
||||
/>
|
||||
<div class="filter-panel-footer clearfix">
|
||||
<template v-if="editingTags === true">
|
||||
<div class="text-center">
|
||||
@@ -405,6 +429,8 @@ import dragIcon from '@/assets/svg/drag_indicator.svg?raw';
|
||||
|
||||
import { mapState, mapActions } from '@/libs/store';
|
||||
import brokenTaskModal from './brokenTaskModal';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -414,10 +440,12 @@ export default {
|
||||
spells,
|
||||
brokenTaskModal,
|
||||
draggable,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
directives: {
|
||||
markdown,
|
||||
},
|
||||
mixins: [autoCompleteHelperMixin],
|
||||
data () {
|
||||
return {
|
||||
columns: ['habit', 'daily', 'todo', 'reward'],
|
||||
@@ -445,10 +473,19 @@ export default {
|
||||
newTag: null,
|
||||
editingTask: null,
|
||||
creatingTask: null,
|
||||
textbox: null,
|
||||
activeTagIndex: -1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
activeTagText () {
|
||||
if (this.activeTagIndex === -1) {
|
||||
return this.newTag || '';
|
||||
}
|
||||
const tag = this.tagsSnap.tags[this.activeTagIndex];
|
||||
return tag ? tag.name || '' : '';
|
||||
},
|
||||
tagsByType () {
|
||||
const userTags = this.user.tags;
|
||||
const tagsByType = {
|
||||
@@ -514,6 +551,43 @@ export default {
|
||||
this.tagsSnap[key].push({ id: uuid(), name: this.newTag });
|
||||
this.newTag = null;
|
||||
},
|
||||
setActiveTag (index) {
|
||||
this.activeTagIndex = index;
|
||||
if (index === -1) {
|
||||
const refArr = this.$refs.newTagInput;
|
||||
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
|
||||
} else {
|
||||
const refArr = this.$refs[`tagInput-${index}`];
|
||||
if (!refArr) {
|
||||
this.textbox = null;
|
||||
} else {
|
||||
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
|
||||
}
|
||||
}
|
||||
},
|
||||
newTagEnterHandler (e, key) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac && ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
ac.makeSelection();
|
||||
} else {
|
||||
if (ac) ac.cancel();
|
||||
this.addTag(e, key);
|
||||
}
|
||||
},
|
||||
selectedTagAutocomplete (newText, newCaret) {
|
||||
if (this.activeTagIndex === -1) {
|
||||
this.newTag = newText;
|
||||
} else {
|
||||
this.tagsSnap.tags[this.activeTagIndex].name = newText;
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (this.textbox) {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
}
|
||||
});
|
||||
},
|
||||
removeTag (index, key) {
|
||||
const tagId = this.tagsSnap[key][index].id;
|
||||
const indexInSelected = this.selectedTags.indexOf(tagId);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div
|
||||
class="selectable-card"
|
||||
:class="{ selected }"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div
|
||||
v-if="selected"
|
||||
class="checkmark-corner"
|
||||
>
|
||||
<div
|
||||
class="svg-icon check-icon"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.selectable-card {
|
||||
position: relative;
|
||||
background: $white;
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 1px 2px 0px rgba(26, 24, 29, 0.08);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 2px solid $purple-300;
|
||||
padding: 15px;
|
||||
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.checkmark-corner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-style: solid;
|
||||
border-width: 48px 48px 0 0;
|
||||
border-color: $purple-300 transparent transparent transparent;
|
||||
border-radius: 6px 0 0 0;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import svgCheck from '@/assets/svg/check.svg?raw';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
check: svgCheck,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -398,14 +398,29 @@
|
||||
:placeholder="$t('imageUrl')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="position: relative;">
|
||||
<label>{{ $t('about') }}</label>
|
||||
<textarea
|
||||
ref="blurbTextarea"
|
||||
v-model="editingProfile.blurb"
|
||||
class="form-control"
|
||||
rows="5"
|
||||
:placeholder="$t('displayBlurbPlaceholder')"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
></textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="editingProfile.blurb"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
<!-- include ../../shared/formatting-help-->
|
||||
</div>
|
||||
</div>
|
||||
@@ -1001,6 +1016,8 @@ import mute from '@/assets/svg/mute.svg?raw';
|
||||
import shadowMute from '@/assets/svg/shadow-mute.svg?raw';
|
||||
import externalLinks from '../../mixins/externalLinks';
|
||||
import { userCustomStateMixin } from '../../mixins/userState';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
|
||||
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
|
||||
|
||||
@@ -1012,8 +1029,9 @@ export default {
|
||||
MemberDetails,
|
||||
profileStats,
|
||||
toggleSwitch,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
|
||||
mixins: [externalLinks, userCustomStateMixin('userLoggedIn'), autoCompleteHelperMixin],
|
||||
props: ['userId', 'startingPage'],
|
||||
data () {
|
||||
return {
|
||||
@@ -1033,6 +1051,7 @@ export default {
|
||||
mute,
|
||||
shadowMute,
|
||||
}),
|
||||
textbox: null,
|
||||
userIdToMessage: '',
|
||||
editing: false,
|
||||
editingProfile: {
|
||||
@@ -1121,6 +1140,13 @@ export default {
|
||||
userLoggedIn () {
|
||||
this.loadUser();
|
||||
},
|
||||
editing (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.textbox = this.$refs.blurbTextarea;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.loadUser();
|
||||
@@ -1331,6 +1357,13 @@ export default {
|
||||
this.$emit('toggled', this.isOpened);
|
||||
},
|
||||
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.editingProfile.blurb = newText;
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
reportPlayer () {
|
||||
this.$root.$emit('habitica::report-profile', {
|
||||
memberId: this.user._id,
|
||||
@@ -1340,7 +1373,7 @@ export default {
|
||||
},
|
||||
|
||||
openAdminPanel () {
|
||||
this.$router.push(`/admin-panel/${this.hero._id}`);
|
||||
this.$router.push(`/admin/panel/${this.hero._id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<strong>{{ $t('equipment') }}:</strong>
|
||||
<span :class="{ 'positive-stat': statsComputed.gearBonus[stat] !== 0 }">
|
||||
{{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{
|
||||
statsComputed.gearBonus[stat]
|
||||
statsComputed.gearBonus[stat] + statsComputed.classBonus[stat]
|
||||
}}
|
||||
</span>
|
||||
</li>
|
||||
@@ -246,7 +246,9 @@
|
||||
:class="{white: user.preferences.background}"
|
||||
style="overflow:hidden"
|
||||
>
|
||||
<Sprite :image-name="'icon_background_' + user.preferences.background" />
|
||||
<Sprite
|
||||
v-if="user.preferences.background && user.preferences.background !== ''"
|
||||
:image-name="'icon_background_' + user.preferences.background" />
|
||||
</div>
|
||||
<b-popover
|
||||
v-if="label !== 'skip'
|
||||
|
||||
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
|
||||
import Vue from 'vue';
|
||||
import getStore from '@/store';
|
||||
|
||||
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
|
||||
const { AMPLITUDE_KEY } = import.meta.env;
|
||||
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
|
||||
|
||||
let analyticsLoading = false;
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
|
||||
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
|
||||
|
||||
import moment from 'moment';
|
||||
import i18n from '@/../../common/script/i18n';
|
||||
|
||||
function loadLocale (i18nData) {
|
||||
// Load i18n strings
|
||||
i18n.strings = i18nData.strings;
|
||||
|
||||
// Load Moment.js locale
|
||||
const { language } = i18nData;
|
||||
|
||||
if (language && i18nData.momentLang && language.momentLangCode) {
|
||||
// Make moment available under `window` so that the locale can be set
|
||||
window.moment = moment;
|
||||
|
||||
// Execute the script and set the locale
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.text = i18nData.momentLang;
|
||||
head.appendChild(script);
|
||||
moment.updateLocale(language.momentLangCode);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
NavbarPlugin,
|
||||
CollapsePlugin,
|
||||
} from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import { setUpLogging } from '@/libs/logging';
|
||||
import router from './router/index';
|
||||
@@ -44,7 +43,6 @@ Vue.use(FormRadioPlugin);
|
||||
Vue.use(TooltipPlugin);
|
||||
Vue.use(NavbarPlugin);
|
||||
Vue.use(CollapsePlugin);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
const store = getStore();
|
||||
|
||||
@@ -15,46 +15,60 @@ export const autoCompleteHelperMixin = {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
_getActiveAutocomplete () {
|
||||
if (this.$refs.autocomplete && this.$refs.autocomplete.searchActive) {
|
||||
return this.$refs.autocomplete;
|
||||
}
|
||||
if (this.$refs.emojiAutocomplete && this.$refs.emojiAutocomplete.searchActive) {
|
||||
return this.$refs.emojiAutocomplete;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleTab (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) {
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
ac.selectPrevious();
|
||||
} else {
|
||||
this.$refs.autocomplete.selectNext();
|
||||
ac.selectNext();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinHandleEscape (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.cancel();
|
||||
ac.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectNextAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectNext();
|
||||
ac.selectNext();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectPreviousAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.selectPrevious();
|
||||
ac.selectPrevious();
|
||||
}
|
||||
},
|
||||
|
||||
autoCompleteMixinSelectAutocomplete (e) {
|
||||
if (this.$refs.autocomplete.searchActive) {
|
||||
if (this.$refs.autocomplete.selected !== null) {
|
||||
const ac = this._getActiveAutocomplete();
|
||||
if (ac) {
|
||||
if (ac.selected !== null) {
|
||||
e.preventDefault();
|
||||
this.$refs.autocomplete.makeSelection();
|
||||
ac.makeSelection();
|
||||
} else {
|
||||
// no autocomplete selected, newline instead
|
||||
this.$refs.autocomplete.cancel();
|
||||
ac.cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,56 +1,14 @@
|
||||
import includes from 'lodash/includes';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
foolPet (pet, prank) {
|
||||
const SPECIAL_PETS = [
|
||||
'Bear-Veteran',
|
||||
'BearCub-Polar',
|
||||
'Cactus-Veteran',
|
||||
'Dragon-Hydra',
|
||||
'Dragon-Veteran',
|
||||
'Fox-Veteran',
|
||||
'Gryphatrice-Jubilant',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphon-RoyalPurple',
|
||||
'Hippogriff-Hopeful',
|
||||
'Jackalope-RoyalPurple',
|
||||
'JackOLantern-Base',
|
||||
'JackOLantern-Ghost',
|
||||
'JackOLantern-Glow',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Lion-Veteran',
|
||||
'MagicalBee-Base',
|
||||
'Mammoth-Base',
|
||||
'MantisShrimp-Base',
|
||||
'Orca-Base',
|
||||
'Phoenix-Base',
|
||||
'Tiger-Veteran',
|
||||
'Turkey-Base',
|
||||
'Turkey-Gilded',
|
||||
'Wolf-Cerberus',
|
||||
'Wolf-Veteran',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'BearCub',
|
||||
'Cactus',
|
||||
'Dragon',
|
||||
'FlyingPig',
|
||||
'Fox',
|
||||
'LionCub',
|
||||
'PandaCub',
|
||||
'TigerCub',
|
||||
'Wolf',
|
||||
];
|
||||
if (!pet) return `Pet-TigerCub-${prank}`;
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return `Pet-Dragon-${prank}`;
|
||||
foolPet (pet, substitutions) {
|
||||
if (!pet || pet === 'Pet-') return substitutions.noPet;
|
||||
if (substitutions[pet]) return substitutions[pet];
|
||||
for (const key in substitutions) {
|
||||
if (pet.startsWith(key)) {
|
||||
return substitutions[key];
|
||||
}
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-${prank}`;
|
||||
}
|
||||
return `Pet-BearCub-${prank}`;
|
||||
return substitutions.default;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import notificationsMixin from '@/mixins/notifications';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
|
||||
const { STRIPE_PUB_KEY } = import.meta.env;
|
||||
|
||||
let stripeInstance = null;
|
||||
|
||||
|
||||
@@ -153,9 +153,23 @@
|
||||
:placeholder="$t('needsTextPlaceholder')"
|
||||
:maxlength="MAX_MESSAGE_LENGTH"
|
||||
:class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
|
||||
@keydown="autoCompleteMixinUpdateCarretPosition"
|
||||
@keyup.ctrl.enter="sendPrivateMessage()"
|
||||
@keydown.tab="autoCompleteMixinHandleTab($event)"
|
||||
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
|
||||
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
|
||||
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
|
||||
@keydown.esc="autoCompleteMixinHandleEscape($event)"
|
||||
>
|
||||
</textarea>
|
||||
<emoji-auto-complete
|
||||
ref="emojiAutocomplete"
|
||||
:text="newMessage"
|
||||
:textbox="textbox"
|
||||
:coords="mixinData.autoComplete.coords"
|
||||
:caret-position="mixinData.autoComplete.caretPosition"
|
||||
@select="selectedAutocomplete"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sub-new-message-row d-flex"
|
||||
@@ -540,6 +554,7 @@ h3 {
|
||||
}
|
||||
|
||||
.new-message-row {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-left: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
@@ -676,6 +691,8 @@ import PmNewMessageStarted from './pm-new-message-started.vue';
|
||||
import StartNewConversationInputHeader from './start-new-conversation-input-header.vue';
|
||||
import positiveIcon from '@/assets/svg/positive.svg?raw';
|
||||
import NotificationMixins from '@/mixins/notifications';
|
||||
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
|
||||
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
|
||||
|
||||
// extract to a shared path
|
||||
const CONVERSATIONS_PER_PAGE = 10;
|
||||
@@ -700,13 +717,14 @@ export default defineComponent({
|
||||
toggleSwitch,
|
||||
userLink,
|
||||
faceAvatar,
|
||||
emojiAutoComplete,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(new Date(value)).fromNow();
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper, NotificationMixins],
|
||||
mixins: [styleHelper, NotificationMixins, autoCompleteHelperMixin],
|
||||
beforeRouteEnter (to, from, next) {
|
||||
next(vm => {
|
||||
const data = vm.$store.state.privateMessageOptions;
|
||||
@@ -751,6 +769,7 @@ export default defineComponent({
|
||||
/** @type {Record<string, PrivateMessages.PrivateMessageEntry[]>} */
|
||||
messagesByConversation: {}, // cache {uuid: []}
|
||||
|
||||
textbox: null,
|
||||
newMessage: '',
|
||||
messages: [],
|
||||
messagesLoading: false,
|
||||
@@ -963,6 +982,15 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
shouldShowInputPanel (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => {
|
||||
this.textbox = this.$refs.textarea;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('messages'),
|
||||
@@ -1224,6 +1252,13 @@ export default defineComponent({
|
||||
triggerStartNewConversationState () {
|
||||
this.showStartNewConversationInput = true;
|
||||
},
|
||||
selectedAutocomplete (newText, newCaret) {
|
||||
this.newMessage = newText;
|
||||
this.$nextTick(() => {
|
||||
this.textbox.setSelectionRange(newCaret, newCaret);
|
||||
this.textbox.focus();
|
||||
});
|
||||
},
|
||||
async startConversationByUsername (targetUserName) {
|
||||
// check if the target user exists in current conversations, select that conversation
|
||||
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -90,7 +90,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment v-if="allowedToChangeClass">
|
||||
<div class="d-content" v-if="allowedToChangeClass">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -83,7 +83,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr>
|
||||
<td class="settings-label">
|
||||
{{ $t("showHeader") }}
|
||||
@@ -26,7 +26,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-for="network in SOCIAL_AUTH_NETWORKS"
|
||||
:key="network.key"
|
||||
@@ -39,7 +39,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -66,7 +66,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -48,7 +48,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<fragment>
|
||||
<div class="d-content">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -76,7 +76,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</fragment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -295,6 +295,10 @@ export default {
|
||||
appState = JSON.parse(appState);
|
||||
if (appState.paymentCompleted) {
|
||||
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
|
||||
if (appState.paymentType === 'groupPlan') {
|
||||
this.$store.state.upgradingGroup = {};
|
||||
this.$store.dispatch('guilds:getGroupPlans', true);
|
||||
}
|
||||
this.$root.$emit('habitica:payment-success', appState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,56 +11,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
|
||||
|
||||
// NOTE: when adding a page make sure to implement the `common:setTitle` action
|
||||
|
||||
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
|
||||
const Logout = () => import('@/components/auth/logout');
|
||||
|
||||
// Hall
|
||||
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
|
||||
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
|
||||
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
|
||||
const HallPage = () => import('@/components/hall/index');
|
||||
const PatronsPage = () => import('@/components/hall/patrons');
|
||||
const HeroesPage = () => import('@/components/hall/heroes');
|
||||
|
||||
// Admin Pages
|
||||
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||
const AdminContainerPage = () => import('@/components/admin/container');
|
||||
const AdminPanelPage = () => import('@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import('@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import('@/components/admin/blocker');
|
||||
|
||||
// Tasks
|
||||
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
|
||||
const UserTasks = () => import('@/components/tasks/user');
|
||||
|
||||
// Inventory
|
||||
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
|
||||
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
|
||||
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
|
||||
const InventoryContainer = () => import('@/components/inventory/index');
|
||||
const ItemsPage = () => import('@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import('@/components/inventory/equipment/index');
|
||||
const StablePage = () => import('@/components/inventory/stable/index');
|
||||
|
||||
// Guilds & Parties
|
||||
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
|
||||
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
|
||||
const GroupPage = () => import('@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
|
||||
const LookingForParty = () => import('@/components/groups/lookingForParty');
|
||||
|
||||
// Group Plans
|
||||
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
|
||||
const GroupPlanIndex = () => import('@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import('@/components/group-plans/billing');
|
||||
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
|
||||
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
|
||||
|
||||
// Challenges
|
||||
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
|
||||
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
|
||||
const ChallengeIndex = () => import('@/components/challenges/index');
|
||||
const MyChallenges = () => import('@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import('@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
|
||||
|
||||
// Shops
|
||||
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
|
||||
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
|
||||
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
|
||||
const ShopsContainer = () => import('@/components/shops/index');
|
||||
const MarketPage = () => import('@/components/shops/market/index');
|
||||
const QuestsPage = () => import('@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import('@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import('@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -85,6 +85,13 @@ const router = new VueRouter({
|
||||
props: true,
|
||||
},
|
||||
{ name: 'profile', path: '/user/profile' },
|
||||
{
|
||||
name: 'avatar',
|
||||
path: '/avatar',
|
||||
children: [
|
||||
{ name: 'backgrounds', path: 'backgrounds' },
|
||||
],
|
||||
},
|
||||
{ name: 'stats', path: '/user/stats' },
|
||||
{ name: 'achievements', path: '/user/achievements' },
|
||||
{
|
||||
@@ -410,6 +417,13 @@ router.beforeEach(async (to, from, next) => {
|
||||
router.app.$root.$emit('bv::hide::modal', 'profile');
|
||||
}
|
||||
|
||||
if (to.name === 'backgrounds') {
|
||||
store.state.avatarEditorOptions.editingUser = true;
|
||||
store.state.avatarEditorOptions.startingPage = 'backgrounds';
|
||||
router.app.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||
return null;
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
|
||||
const OverviewPage = () => import('@/components/static/overview');
|
||||
const PressKitPage = () => import('@/components/static/pressKit');
|
||||
const PrivacyPage = () => import('@/components/static/privacy');
|
||||
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
|
||||
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import('@/components/auth/registerUsername');
|
||||
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
|
||||
const TermsPage = () => import('@/components/static/terms');
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ export async function appleAuth (store, params) {
|
||||
id_token: params.idToken,
|
||||
name: params.name,
|
||||
username: params.username,
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -109,7 +110,10 @@ export async function appleAuth (store, params) {
|
||||
}
|
||||
|
||||
if (result.data.message && result.data.id_token) {
|
||||
return { idToken: result.data.id_token };
|
||||
return {
|
||||
idToken: result.data.id_token,
|
||||
email: result.data.email,
|
||||
};
|
||||
}
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
@@ -122,7 +122,7 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
experimentalMinChunkSize: 1000
|
||||
experimentalMinChunkSize: 20000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -104,15 +104,15 @@
|
||||
"achievementSkeletonCrewModalText": "Posbíral/a jsi všechna kostnatá zvířata!",
|
||||
"achievementSkeletonCrewText": "Posbíral/a všechna kostnatá zvířata.",
|
||||
"achievementLegendaryBestiaryModalText": "Posbíral/a jsi všechny mytické mazlíčky!",
|
||||
"achievementLegendaryBestiaryText": "Posbíral/a jsi všechny základní barvy mytických mazlíčků: draka, létajícího prasete, gryfona, mořského hada a jednorožce!",
|
||||
"achievementLegendaryBestiaryText": "Posbíral/a všechny barvy mytických mazlíčků: drak, létající prase, gryfon, mořský hady a jednorožec!",
|
||||
"achievementLegendaryBestiary": "Legendární bestiář",
|
||||
"achievementSeasonalSpecialist": "Sezónní specialista",
|
||||
"achievementVioletsAreBlueText": "Získal/a všechny cukrově modré mazlíčky.",
|
||||
"achievementVioletsAreBlue": "Fialky jsou Modré",
|
||||
"achievementVioletsAreBlue": "Fialky jsou modré",
|
||||
"achievementVioletsAreBlueModalText": "Posbíral/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
|
||||
"achievementSeasonalSpecialistModalText": "Dokončl/a jsi všechny sezónní úkoly!",
|
||||
"achievementDomesticatedModalText": "Sesbíral/a jsi všechna domácí zvířata!",
|
||||
"achievementSeasonalSpecialistText": "Dokončil/a jsi všechny Jarní a Zimní sezónní úkoly: Honba za vajíčky, Pastičkář Santa, a najdi Cuba!",
|
||||
"achievementSeasonalSpecialistText": "Splnil/a všechny jarní a zimní sezonní úkoly: Lov Vajec, Uvězněný Santa, a Najdi Mládě!",
|
||||
"achievementWildBlueYonderText": "Ochočil/a všechny zvířata z Modré Cukrové Vaty.",
|
||||
"achievementWildBlueYonderModalText": "Ochočil/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
|
||||
"achievementDomesticatedText": "Vylíhl/a všechna standardní zbarvení domácích zvířat: Fretka, morče, kohout, létající prasátko, krysa, králík, kůň a kráva!",
|
||||
@@ -157,5 +157,12 @@
|
||||
"achievementBonelessBossModalText": "Získal/a jsi všechny bezobratlé mazlíčky!",
|
||||
"achievementDuneBuddy": "Kámoš z dun",
|
||||
"achievementDuneBuddyText": "Vylíhl/a jsi všechny, v poušti se vyskytující, mazlíčky: pásovce, kaktus, lišku, žábu, hada a pavouka!",
|
||||
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!"
|
||||
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!",
|
||||
"achievementRodentRulerText": "Vylíhly se všechny standardní barvy hlodavců: morčata, krysy a veverky!",
|
||||
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
|
||||
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
|
||||
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
|
||||
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!",
|
||||
"achievementRodentRuler": "Vládce hlodavců",
|
||||
"achievementCats": "Pasák koček"
|
||||
}
|
||||
|
||||
@@ -735,5 +735,8 @@
|
||||
"backgroundMaskMakersWorkshopText": "Maskářova dílna",
|
||||
"backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.",
|
||||
"backgroundCemeteryGateText": "Hřbitovní brána",
|
||||
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány."
|
||||
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány.",
|
||||
"backgroundAutumnBridgeText": "Podzimní most",
|
||||
"backgroundAutumnBridgeNotes": "Obdivuj krásu podzimního mostu.",
|
||||
"backgroundInsideACrystalText": "Uvnitř krystalu."
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"brokenChaLink": "Nefunkční odkaz na výzvu",
|
||||
"keepIt": "Ponechat",
|
||||
"removeIt": "Odstranit",
|
||||
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
|
||||
"brokenChallenge": "Neplatný odkaz na výzvu",
|
||||
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
|
||||
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
|
||||
"challenges": "Výzvy",
|
||||
@@ -85,7 +85,7 @@
|
||||
"summaryRequired": "Je požadováno shrnutí",
|
||||
"summaryTooLong": "Shrnutí je příliš dlouhé",
|
||||
"descriptionRequired": "Je požadován popis",
|
||||
"locationRequired": "Je požadováno vybrat lokaci výzvy ('Přidat k')",
|
||||
"locationRequired": "Je nutné vybrat umístění výzvy („Přidat do“)",
|
||||
"categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
|
||||
"viewProgressOf": "Zobrazit pokrok",
|
||||
"viewProgress": "Zobrazit pokrok",
|
||||
@@ -94,5 +94,16 @@
|
||||
"selectParticipant": "Zvol účastníka",
|
||||
"filters": "Filtry",
|
||||
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
|
||||
"yourReward": "Tvá odměna"
|
||||
"yourReward": "Tvá odměna",
|
||||
"brokenTaskDescription": "Tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš udělat?",
|
||||
"brokenChallengeDescription": "Tento úkol byl součástí výzvy, ale výzva (nebo skupina) byla smazána. Co chceš udělat s osiřelými úkoly?",
|
||||
"challengeCompletedDescription": "Vítězem je <%= user %>! Co chceš udělat s osiřelými úkoly?",
|
||||
"messageChallengeFlagAlreadyReported": "Tuto výzvu jsi už nahlásil.",
|
||||
"flaggedNotHidden": "Výzva byla nahlášena jednou, není skrytá",
|
||||
"flaggedAndHidden": "Výzva byla nahlášena a je skrytá",
|
||||
"resetFlagCount": "Resetovat počet nahlášení",
|
||||
"deleteChallengeRefundDescription": "Pokud tuto výzvu smažeš, bude ti vrácena odměna v drahokamech a úkoly z výzvy zůstanou na nástěnkách úkolů účastníků.",
|
||||
"messageChallengeFlagOfficial": "Oficiální výzvy nelze nahlásit.",
|
||||
"brokenTask": "Nefunkční odkaz na výzvu",
|
||||
"removeTasks": "Odstranit Úkoly"
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
"iosFaqStillNeedHelp": "Jestli máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), použij formulář Ask a Question v sekci Nápověda na horní liště rozhraní. Jsme rádi když můžeme pomoct.",
|
||||
"androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
|
||||
"webFaqStillNeedHelp": "Pokud máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), přijď se zeptat do [Cechu „Habitica Help‟](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! Rádi ti pomůžeme.",
|
||||
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví. \n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
|
||||
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví.\n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
|
||||
"webFaqAnswer26": "Pozitivní návyky (návyky, které chceš udržovat; měly by mít tlačítko plus)\n\n * Sněz vitamíny\n * Vyčisti si zuby\n * Hodina učení se\n\nNegativní návyky (návyky které chceš omezit nebo se jim zcela vyhnout; měly by mít tlačítko mínus)\n\n * Kouření\n * Bezmyšlenkovité scrollování\n * Kousání si nehtů\n\nOboustranné návyky (Návyky které mají jak pozitivní, tak negativní možnost; měly by mít tlačítko plus i mínus)\n\n * Pít vodu vs. Pít limonádu\n * Učit se vs. prokrastinovat\n\nNávrhy denních úkolů (úkoly, které chceš plnit pravidelně)\n * Umýt nádobí\n * Zalít kytky\n * 30 minut nějaké fyzické aktivity\n\nNávrhy úkolů do Úkolníčku (úkoly co chceš splnit jen jednou)\n\n * Objednat se k doktorovi\n * Zorganizovat obsah skříně\n * Dopsat esej",
|
||||
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Stáj” and sjeď dolů, k sekci Stáj",
|
||||
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Domácí zvířata a mounti” and sjeď dolů, k sekci Stáj",
|
||||
"commonQuestions": "Časté otázky",
|
||||
"faqQuestion25": "Jaké různé úkoly existují?",
|
||||
"faqQuestion26": "Jaké úkoly mohu například vytvořit?",
|
||||
@@ -13,19 +13,25 @@
|
||||
"faqQuestion29": "Jak získám zpět Zdraví?",
|
||||
"webFaqAnswer29": "Můžeš získat 15 bodů zdraví zakoupením Lektvaru zdraví ze sloupce Odměny za 25 zlaťáků. Navíc, pokud postoupíš do další úrovně, tak se ti všechno zdraví automaticky obnoví!",
|
||||
"faqQuestion30": "Co se stane, když mi dojde zdraví?",
|
||||
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit.",
|
||||
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit. Můžete se znovu postavit plněním úkolů a opětovným zvyšováním úrovně.",
|
||||
"faqQuestion31": "Proč jsem ztratil body, když jsem řešil úkol, který nebyl negativní?",
|
||||
"webFaqAnswer31": "Když doděláš úkol a ztratíš zdraví i když bys správně neměl, narazil jsi na zpoždění, během kterého server synchronizoval změny na jiných platformách. Například, pokud použiješ zlaťáky, manu nebo ztratíš zkušenosti na aplikaci na mobilu a pak dokončíš akci na webově stránce, server jednoduše potvrzuje, že se všechno synchronizovalo.",
|
||||
"faqQuestion32": "Kdy si mohu vybrat třídu?",
|
||||
"webFaqAnswer32": "V Habitice existují čtyři třídy: Válečník, Mág, Zloděj a Léčitel. Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.",
|
||||
"faqQuestion32": "Jak si mohu vybrat kurz?",
|
||||
"webFaqAnswer32": "Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.\n\nPokud chcete změnit svou třídu po dosažení úrovně 10, můžete tak učinit pomocí Koule znovuzrození. Koule znovuzrození je k dispozici na trhu za 6 drahokamů na úrovni 50 nebo zdarma na úrovni 100.\n\nAlternativně můžete změnit třídu kdykoli v nastavení za 3 drahokamy. Tím se vaše úroveň nevynuluje jako v případě Koule znovuzrození, ale budete moci přerozdělit body dovedností, které jste nashromáždili při postupu na vyšší úroveň, tak aby odpovídaly vaší nové třídě.",
|
||||
"faqQuestion33": "Co je to za modrou čáru s popisem Mana, která se objeví po dosažení 10. úrovně?",
|
||||
"webFaqAnswer33": "Poté, co odemkneš třídní systém, tak odemkneš i schopnosti, jež ke svému použití vyžadují manu. Mana je učena tvou INT (inteligencí) a dá se měnit pomocí schopností a vybavení.",
|
||||
"faqQuestion34": "Jaký typ jídla má rád můj mazlíček?",
|
||||
"webFaqAnswer34": "Mazlíčci mají rádi jídla, která jim jdou barevně k srsti. Základní mazlíčci jsou výjimka, ale všichni základní mazlíčci mají rádi stejný předmět. Dole vidíš jídla, která mají specifičtí mazlíčci rádi:\n\n * Základní mazlíčci mají rádi maso\n * Bílí mazlíčci mají rádi mléko\n * Pouštní mazlíčci mají rádi brambory\n * Červení mazlíčci mají rádi jahody\n * Stínoví mazlíčci mají rádi čokoládu\n * Kostnatí mazlíčci mají rádi ryby\n * Zombie mazlíčci mají rádi hnijící maso\n * Cukrově růžoví mazlíčci mají rádi růžovou cukrovou vatu\n * Cukrově modří mazlíčci mají rádi modrou cukrovou vatu\n * Zlatí mazlíčci mají rádi med",
|
||||
"faqQuestion35": "Nakrmil jsem svého mazlíčka a on zmizel! Co se stalo?",
|
||||
"faqQuestion36": "Jak mohu změnit vzhled své postavy?",
|
||||
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Upravit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Upravit postavu\"",
|
||||
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Přizpůsobit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Přizpůsobit postavu\"",
|
||||
"faqQuestion27": "Proč úkoly mění barvy?",
|
||||
"webFaqAnswer27": "Barva úkolu je vizuální ukázkou hodnoty úkolu. Všechny úkoly začínají neutrálně žlutě, modrá je lepší a červená horší. Zde uvidíš jak typ úkolu určuje hodnotu úkolu:\n\nNávyky zmodrají nebo zčervenají podle toho, jestli klikneš na tlačítko plus nebo mínus. Pokud je nebudeš plnit, tak pozitivní a negativní úkoly oslabíš až na žlutou. Dvojité návyky mění barvy pouze na základě tvých zadání.\n\nDenní úkoly mění barvu podle toho, jak často jsou plněny a když se plní, stávají se modřejšími, nebo pokud jsou zanedbány, zčervenají.\n\nČím déle jsou úkoly v úkolníčku nesplněné, tím červenějšími se stávají.\n\nČím červenější úkol, tím víc zlaťáků a zkušeností získáš za jeho splnění, takže se vrhni i na ty nejdrsnější úkoly!",
|
||||
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?"
|
||||
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?",
|
||||
"faqQuestion37": "Proč se mé vybavení neukazuje na mé postavě?",
|
||||
"webFaqAnswer37": "Zkontrolujte, zda je zapnutá možnost Kostým. Pokud má váš avatar na sobě kostým, zobrazí se místo vaší bojové výstroje tato sada vybavení.\n\nZapnutí kostýmu v mobilních aplikacích:\n * V nabídce vyberte „Vybavení“ a najděte přepínač Kostým.\n\nZapnutí kostýmu na webových stránkách:\n * V inventáři vyberte „Vybavení“ a najděte přepínač Kostým v záložce Kostým v zásuvce Vybavení",
|
||||
"faqQuestion38": "Proč nemohu zakoupit určité položky?",
|
||||
"webFaqAnswer38": "Noví hráči Habitica mohou zakoupit pouze základní vybavení třídy válečník. Hráči musí nakupovat vybavení v pořadí, aby odemkli další kus.\n\nMnoho kusů vybavení je specifických pro danou třídu, což znamená, že hráč může zakoupit pouze vybavení patřící k jeho aktuální třídě.",
|
||||
"faqQuestion39": "Kde mohu získat další vybavení?",
|
||||
"faqQuestion40": "Co jsou gemy a jak je dostanu?"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"rebirthOrb": "Použil Kouli Znovozrození, aby začal znova, po dosáhnutí úrovně <%= level %>.",
|
||||
"rebirthOrb100": "Použil Kouli znovuzrození, aby začal odznovu po dosažení úrovně 100 nebo vyšší.",
|
||||
"rebirthOrbNoLevel": "Použil Kouli Znovozrození, aby začal znova.",
|
||||
"rebirthPop": "Obnoví tvou postavu a vrátí jí na 1. úroveň s povoláním Válečníka, zatímco ti zůstanou všechny úspěchy, celá sbírka a vybavení. Tvoje úkoly zůstanou i s historií, ale vrátí se na žlutou barvu. Tvé řady úspěchů se resetují, kromě úkolů patřících do aktivních výzev či do Skupiny. Tvé zlato, zkušenosti, mana a efekty všech schopností budou odstraněny. Toto vše nastane s okamžitou platností. Pro více informací se podívej na wiki stránku: <a href='https://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Orb znovuzrození</a>.",
|
||||
"rebirthPop": "Obnoví tvou postavu a vrátí jí na 1. úroveň s povoláním Válečníka, zatímco ti zůstanou všechny úspěchy, celá sbírka a vybavení. Tvoje úkoly zůstanou i s historií, ale vrátí se na žlutou barvu. Tvé řady úspěchů se resetují, kromě úkolů patřících do aktivních výzev či do Skupiny. Tvé zlato, zkušenosti, mana a efekty všech schopností budou odstraněny. Toto vše nastane s okamžitou platností.",
|
||||
"rebirthName": "Koule znovuzrození",
|
||||
"rebirthComplete": "Byl jste znovuzrozen!",
|
||||
"nextFreeRebirth": "<strong><%= days %> dni</strong> do <strong>bezplatného</strong> Koule znovuzrození"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user