mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-21 03:08:29 -05:00
Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9aed479296 | |||
| 47c156d9b1 | |||
| e6418e4356 | |||
| 3c23989e99 | |||
| 2d1f341256 | |||
| 44adfd611a | |||
| ab50c41287 | |||
| c43abe82fe | |||
| ac0b4a324f | |||
| efa0a325a2 | |||
| bc970d33ac | |||
| 09e432cf32 | |||
| 40aa2e214d | |||
| 9f563b741d | |||
| 9db541f4c3 | |||
| ce4a20e3d8 | |||
| 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 | |||
| a542277a41 | |||
| cdf8556fd6 | |||
| 3d93390a7a | |||
| 59f9cfa0f4 | |||
| 80d7804f69 | |||
| 4e5efe09a3 | |||
| d42a597672 | |||
| ea17b2e9c7 | |||
| f56708cd88 | |||
| 005d14f6e8 | |||
| c05a96ce6c | |||
| 8fdbfb9dc6 | |||
| 057a642baa | |||
| 6c522157a7 | |||
| ba9a1ab2a9 | |||
| 4767461c4f | |||
| 847c97dc8f | |||
| 215b26acac | |||
| e223e7821a | |||
| 8134fa7c00 | |||
| 5c555cbf88 | |||
| 7379c7b230 | |||
| c055537c38 | |||
| 7559feec8e | |||
| 43808696a8 | |||
| 72fb41c7e0 | |||
| 3bf18e09ed |
@@ -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);
|
||||
|
||||
|
||||
+61
-26
@@ -6,9 +6,21 @@ gulp.task('cache:content', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const {
|
||||
CONTENT_CACHE_PATH,
|
||||
getLocalizedContentResponse,
|
||||
IOS_FILTER,
|
||||
ANDROID_FILTER,
|
||||
buildFilterObject,
|
||||
hashForFilter,
|
||||
} = require('../website/server/libs/content'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
const iosHash = hashForFilter(IOS_FILTER);
|
||||
const iosFilterObj = buildFilterObject(IOS_FILTER);
|
||||
const androidHash = hashForFilter(ANDROID_FILTER);
|
||||
const androidFilterObj = buildFilterObject(ANDROID_FILTER);
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
@@ -26,33 +38,56 @@ gulp.task('cache:content', done => {
|
||||
getLocalizedContentResponse(langCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
gulp.task('cache:i18n', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { BROWSER_SCRIPT_CACHE_PATH, geti18nBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folder (if it doesn't exist)
|
||||
try {
|
||||
fs.mkdirSync(BROWSER_SCRIPT_CACHE_PATH);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
|
||||
// create and save the i18n browser script for each language
|
||||
langCodes.forEach(languageCode => {
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}${languageCode}.js`,
|
||||
geti18nBrowserScript(languageCode),
|
||||
`${CONTENT_CACHE_PATH}${langCode}${iosHash}.json`,
|
||||
getLocalizedContentResponse(langCode, iosFilterObj),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
`${CONTENT_CACHE_PATH}${langCode}${androidHash}.json`,
|
||||
getLocalizedContentResponse(langCode, androidFilterObj),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
done();
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
});
|
||||
|
||||
function safeMkdir (path) {
|
||||
try {
|
||||
fs.mkdirSync(path);
|
||||
} catch (err) {
|
||||
if (err.code !== 'EEXIST') throw err;
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('cache:i18n', done => {
|
||||
// Requiring at runtime because these files access `common`
|
||||
// code which in production works only if transpiled so after
|
||||
// gulp build:babel:common has run
|
||||
const { BROWSER_SCRIPT_CACHE_PATH, geti18nCoreBrowserScript, geti18nContentBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
|
||||
|
||||
try {
|
||||
// create the cache folders (if they doesn't exist)
|
||||
safeMkdir(BROWSER_SCRIPT_CACHE_PATH);
|
||||
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}core/`);
|
||||
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}content/`);
|
||||
|
||||
// create and save the i18n browser script for each language
|
||||
langCodes.forEach(languageCode => {
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}core/${languageCode}.js`,
|
||||
geti18nCoreBrowserScript(languageCode),
|
||||
'utf8',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
`${BROWSER_SCRIPT_CACHE_PATH}content/${languageCode}.js`,
|
||||
geti18nContentBrowserScript(languageCode),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
+827
-704
File diff suppressed because it is too large
Load Diff
+13
-7
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.43.2",
|
||||
"version": "5.46.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,6 +19,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bullmq": "^5.71.1",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -39,19 +40,23 @@
|
||||
"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",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
"on-headers": "^1.1.0",
|
||||
@@ -63,7 +68,6 @@
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
@@ -100,13 +104,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"
|
||||
@@ -122,7 +129,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"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable global-require */
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
import worker from '../../../../website/server/libs/worker';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
@@ -127,7 +127,7 @@ describe('emails', () => {
|
||||
let sendTxn = null;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
sandbox.stub(worker, 'sendJob').returns(defer().promise);
|
||||
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('IS_PROD').returns(true);
|
||||
@@ -149,13 +149,12 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -168,7 +167,7 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).not.to.be.called;
|
||||
expect(worker.sendJob).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
@@ -233,13 +232,12 @@ describe('emails', () => {
|
||||
const mailingInfo = getUser();
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -253,15 +251,14 @@ describe('emails', () => {
|
||||
const variables = [];
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
&& value[0].vars[1].name === 'RECIPIENT_UNSUB_URL', 'matches personal variables'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -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>');
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
|
||||
it('returns four messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
promises.push(user.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: 'fourth',
|
||||
|
||||
@@ -193,23 +193,6 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
expect(questingGroup.quest.members[notInPartyUser._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who have been deleted from quest.members', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].del('/user', {
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who don\'t have true value in quest.members from quest.members', async () => {
|
||||
const partyMemberThatRejects = partyMembers[1];
|
||||
const partyMemberThatIgnores = partyMembers[2];
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {
|
||||
each,
|
||||
map,
|
||||
} from 'lodash';
|
||||
import {
|
||||
checkExistence,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateUser,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
@@ -15,6 +9,7 @@ import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import sendJob from '../../../../../website/server/libs/worker';
|
||||
|
||||
const DELETE_CONFIRMATION = 'DELETE';
|
||||
|
||||
@@ -47,12 +42,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('sends deletion job to worker', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
@@ -84,53 +80,6 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user\'s tasks', async () => {
|
||||
await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
// gets the user's tasks ids
|
||||
const ids = [];
|
||||
each(user.tasksOrder, idsForOrder => {
|
||||
ids.push(...idsForOrder);
|
||||
});
|
||||
|
||||
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
|
||||
});
|
||||
|
||||
it('reduces memberCount in challenges user is linked to', async () => {
|
||||
const populatedGroup = await createAndPopulateGroup({
|
||||
members: 2,
|
||||
});
|
||||
|
||||
const { group } = populatedGroup;
|
||||
const authorizedUser = populatedGroup.members[1];
|
||||
|
||||
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
||||
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(2);
|
||||
|
||||
await authorizedUser.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
@@ -158,10 +107,10 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const textPassword = 'mySecretPassword';
|
||||
const salt = sha1MakeSalt();
|
||||
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
|
||||
await user.updateOne({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
@@ -179,7 +128,8 @@ describe('DELETE /user', () => {
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
@@ -213,11 +163,12 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes a Google user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -232,12 +183,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Apple user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('deletes an Apple user', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,6 +65,52 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('includes sanitized version of provided username', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Google User Name',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername');
|
||||
});
|
||||
|
||||
it('generates a random username if provided username contains only disallowed characters', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Áîüè',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('generates a random username if provided username contains a disallowed word', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'i am a TESTPLACEHOLDERSLURWORDHERE',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('generates a random username if sanitized username conflicts with an extant user', async () => {
|
||||
user = await generateUser({ 'auth.local.username': 'GoogleUserName' });
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
username: 'Google User Name',
|
||||
});
|
||||
|
||||
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
|
||||
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
|
||||
});
|
||||
|
||||
it('fails if allowRegister is false and user does not exist', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
|
||||
it('returns five messages when using page-query ', async () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
for (let i = 0; i < 50; i += 1) {
|
||||
promises.push(user.post('/members/send-private-message', {
|
||||
toUserId: user.id,
|
||||
message: 'fourth',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ describe('shared.ops.unlock', () => {
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.balance = usersStartingGems;
|
||||
user.pinnedItems.push({ type: 'background', path: 'backgrounds.backgrounds042016.giant_florals' });
|
||||
user.pinnedItems.push({ type: 'haircolor', path: 'hair.color.rainbow' });
|
||||
user.pinnedItems.push({ type: 'shirt', path: 'shirt.convict' });
|
||||
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
|
||||
});
|
||||
|
||||
@@ -272,6 +275,7 @@ describe('shared.ops.unlock', () => {
|
||||
});
|
||||
|
||||
it('unlocks an item (appearance)', async () => {
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.not.equal(-1);
|
||||
const path = unlockPath.split(',')[0];
|
||||
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
@@ -282,11 +286,12 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('unlocks an item (hair color)', async () => {
|
||||
user.purchased.hair.color = {};
|
||||
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.not.equal(-1);
|
||||
const path = hairUnlockPath.split(',')[0];
|
||||
const initialColorHair = Object.keys(user.purchased.hair.color).length;
|
||||
const [, message] = await unlock(user, { query: { path } });
|
||||
@@ -297,6 +302,7 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, path)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('unlocks an item (facial hair)', async () => {
|
||||
@@ -334,6 +340,7 @@ describe('shared.ops.unlock', () => {
|
||||
|
||||
it('unlocks an item (background)', async () => {
|
||||
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.not.equal(-1);
|
||||
const [, message] = await unlock(user, {
|
||||
query: { path: backgroundUnlockPath },
|
||||
});
|
||||
@@ -344,6 +351,7 @@ describe('shared.ops.unlock', () => {
|
||||
);
|
||||
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.equal(-1);
|
||||
});
|
||||
|
||||
it('handles an invalid hair path gracefully', async () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
import { STRING_ERROR_MSG, STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
|
||||
import { STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
|
||||
import translator from '../../website/common/script/content/translation';
|
||||
|
||||
describe('Translator', () => {
|
||||
it('returns error message if string is not properly formatted', () => {
|
||||
const improperlyFormattedString = translator('petName', { attr: 0 })();
|
||||
expect(improperlyFormattedString).to.match(STRING_ERROR_MSG);
|
||||
});
|
||||
|
||||
it('returns an error message if string does not exist', () => {
|
||||
const stringDoesNotExist = translator('stringDoesNotExist')();
|
||||
expect(stringDoesNotExist).to.match(STRING_DOES_NOT_EXIST_MSG);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import i18n from '../../website/common/script/i18n';
|
||||
import './globals.helper';
|
||||
import { translations } from '../../website/server/libs/i18n';
|
||||
import { contentTranslations } from '../../website/server/libs/i18n';
|
||||
|
||||
i18n.translations = translations;
|
||||
i18n.translations = contentTranslations;
|
||||
|
||||
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
|
||||
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -32,6 +32,6 @@
|
||||
|
||||
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
|
||||
<!-- Translations -->
|
||||
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
|
||||
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Generated
+4979
-2777
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,12 +28,13 @@
|
||||
"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",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.0.0",
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.1",
|
||||
"sass": "^1.63.4",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,57 +1,3 @@
|
||||
.quest_lostMasterclasser4 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_solarSystem {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_virtualpet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_VirtualPet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Fungi {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -80,6 +26,7 @@
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -87,7 +34,6 @@
|
||||
|
||||
/* Critical */
|
||||
.weapon_special_critical {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-left:-12px;
|
||||
@@ -98,6 +44,7 @@
|
||||
.weapon_special_1 {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -106,36 +53,15 @@
|
||||
.back_special_heroicAureole {
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.broad_armor_special_0,.slim_armor_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.broad_armor_special_1,.slim_armor_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
|
||||
@@ -143,36 +69,17 @@
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.eyewear_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.Pet-Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
|
||||
.Pet-Gryphatrice-Jubilant {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -182,39 +89,11 @@
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.background_airship {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_clocktower {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_steamworks {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' }),
|
||||
},
|
||||
|
||||
@@ -422,6 +422,32 @@
|
||||
class="btn btn-secondary"
|
||||
@click="makeAdmin()"
|
||||
>Make Admin</a>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<input
|
||||
v-model.number="partyChatCount"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control form-control-sm mr-2"
|
||||
style="width: 80px;"
|
||||
>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
@click="seedPartyChat()"
|
||||
>Send Party Chat Messages</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center mt-2">
|
||||
<input
|
||||
v-model.number="inboxCount"
|
||||
type="number"
|
||||
min="1"
|
||||
class="form-control form-control-sm mr-2"
|
||||
style="width: 80px;"
|
||||
>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
@click="seedInbox()"
|
||||
>Send Inbox Messages</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -796,6 +822,8 @@ export default {
|
||||
DEBUG_ENABLED,
|
||||
TIME_TRAVEL_ENABLED,
|
||||
lastTimeJump: null,
|
||||
partyChatCount: 450,
|
||||
inboxCount: 450,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -914,6 +942,35 @@ export default {
|
||||
// Reload the website then go to Help > Admin Panel to set contributor level, etc.');
|
||||
// @TODO: sync()
|
||||
},
|
||||
async seedPartyChat () {
|
||||
try {
|
||||
const count = this.partyChatCount;
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
await axios.post('/api/v4/debug/seed-party-chat', { messageCount: count });
|
||||
window.alert(`Successfully sent ${count} messages to your party chat!`); // eslint-disable-line no-alert
|
||||
} catch (e) {
|
||||
window.alert(e.response?.data?.message || 'Error sending party chat messages'); // eslint-disable-line no-alert
|
||||
}
|
||||
},
|
||||
async seedInbox () {
|
||||
try {
|
||||
const count = this.inboxCount;
|
||||
if (!Number.isInteger(count) || count < 1) {
|
||||
window.alert('Please enter a positive integer'); // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
await axios.post('/api/v4/debug/seed-inbox', { messageCount: count });
|
||||
window.alert(`Successfully sent ${count} messages to your inbox!`); // eslint-disable-line no-alert
|
||||
} catch (e) {
|
||||
window.alert(e.response?.data?.message || 'Error sending inbox messages'); // eslint-disable-line no-alert
|
||||
}
|
||||
},
|
||||
donate () {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||
},
|
||||
showBailey () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
|
||||
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.purchased;
|
||||
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.purchased;
|
||||
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[0];
|
||||
} 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
|
||||
@@ -281,6 +281,11 @@
|
||||
.badge-dialog {
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
|
||||
.badge-pin {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -903,8 +908,8 @@ export default {
|
||||
purchaseGems () {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems');
|
||||
},
|
||||
togglePinned () {
|
||||
this.isPinned = this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
|
||||
async togglePinned () {
|
||||
this.isPinned = await this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
|
||||
|
||||
if (!this.isPinned) {
|
||||
this.text(this.$t('unpinnedItem', { item: this.item.text }));
|
||||
|
||||
@@ -76,7 +76,21 @@
|
||||
:empty-item="false"
|
||||
:show-popover="Boolean(ctx.item.text)"
|
||||
@click="selectItem(ctx.item)"
|
||||
/>
|
||||
>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
slot-scope="slotProps"
|
||||
>
|
||||
<span
|
||||
class="badge-top"
|
||||
@click.prevent.stop="togglePinned(slotProps.item)"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="slotProps.item.pinned"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</shop-item>
|
||||
</template>
|
||||
</item-rows>
|
||||
</div>
|
||||
@@ -108,6 +122,16 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.market .badge-pin:not(.pinned) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.market .item:hover .badge-pin {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import shops from '@/../../common/script/libs/shops';
|
||||
@@ -118,7 +142,9 @@ import Checkbox from '@/components/ui/checkbox';
|
||||
import FilterGroup from '@/components/ui/filterGroup';
|
||||
import FilterSidebar from '@/components/ui/filterSidebar';
|
||||
import ItemRows from '@/components/ui/itemRows';
|
||||
import PinBadge from '@/components/ui/pinBadge';
|
||||
import ShopItem from '../shopItem';
|
||||
import pinUtils from '@/mixins/pinUtils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -126,8 +152,10 @@ export default {
|
||||
FilterGroup,
|
||||
FilterSidebar,
|
||||
ItemRows,
|
||||
PinBadge,
|
||||
ShopItem,
|
||||
},
|
||||
mixins: [pinUtils],
|
||||
data () {
|
||||
return {
|
||||
searchText: null,
|
||||
@@ -184,8 +212,12 @@ export default {
|
||||
methods: {
|
||||
customizationsItems (options = {}) {
|
||||
const { category, searchBy } = options;
|
||||
return category.items.filter(item => !searchBy
|
||||
|| item.text.toLowerCase().includes(searchBy));
|
||||
return category.items
|
||||
.filter(item => !searchBy || item.text.toLowerCase().includes(searchBy))
|
||||
.map(item => ({
|
||||
...item,
|
||||
pinned: this.isPinned(item),
|
||||
}));
|
||||
},
|
||||
emptyClick (identifier, event) {
|
||||
if (event.target.tagName !== 'A') return;
|
||||
|
||||
@@ -180,6 +180,11 @@
|
||||
.badge-dialog {
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
|
||||
.badge-pin {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.badge-pin {
|
||||
background-color: $white;
|
||||
color: $gray-200;
|
||||
color: $gray-100;
|
||||
transition: none;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
@@ -32,8 +32,8 @@
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,10 +691,12 @@ 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;
|
||||
const PM_PER_PAGE = 10;
|
||||
const PM_PER_PAGE = 50;
|
||||
|
||||
const UI_STATES = Object.freeze({
|
||||
LOADING: 'LOADING',
|
||||
@@ -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} */
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<div
|
||||
v-once
|
||||
class="feedback"
|
||||
class="feedback mt-3"
|
||||
v-html="$t('feedback')"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -268,7 +268,6 @@ export default {
|
||||
this.$store.dispatch('user:fetch'),
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
let analyticsConsent = localStorage.getItem('analyticsConsent');
|
||||
if (analyticsConsent !== null) {
|
||||
analyticsConsent = analyticsConsent === 'true';
|
||||
@@ -276,31 +275,11 @@ export default {
|
||||
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.updateUser();
|
||||
return axios.get(
|
||||
'/api/v4/i18n/browser-script',
|
||||
{
|
||||
language: this.user.preferences.language,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
},
|
||||
},
|
||||
);
|
||||
return this.loadAllTranslations();
|
||||
}).then(() => {
|
||||
const i18nData = window && window['habitica-i18n'];
|
||||
this.$loadLocale(i18nData);
|
||||
this.$store.state.isUserLoaded = true;
|
||||
this.hideLoadingScreen();
|
||||
|
||||
// Adjust the timezone offset
|
||||
@@ -316,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);
|
||||
}
|
||||
}
|
||||
@@ -380,6 +363,36 @@ export default {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
async loadContentTranslations () {
|
||||
const contentTranslations = await axios.get(
|
||||
'/api/v4/i18n/content',
|
||||
{
|
||||
language: this.user.preferences.language,
|
||||
},
|
||||
);
|
||||
const i18nData = window && window['habitica-i18n'];
|
||||
i18nData.strings = { ...i18nData.strings, ...contentTranslations.data };
|
||||
this.$loadLocale(i18nData);
|
||||
},
|
||||
async loadAllTranslations () {
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return this.loadContentTranslations();
|
||||
}
|
||||
}
|
||||
await axios.get(
|
||||
'/api/v4/i18n/core',
|
||||
{
|
||||
language: this.user.preferences.language,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
},
|
||||
},
|
||||
);
|
||||
return this.loadContentTranslations();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Vue from 'vue';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export async function getChat (store, payload) {
|
||||
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat`);
|
||||
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
|
||||
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
experimentalMinChunkSize: 1000
|
||||
experimentalMinChunkSize: 20000
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"keepIt": "Запазване",
|
||||
"removeIt": "Премахване",
|
||||
"brokenChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но то (или групата) е било изтрито. Какво бихте искали да направите с останалите задачи?",
|
||||
"challengeCompleted": "Това предизвикателство е приключило и победителят е <span class=\"badge\"><%- user %></span>! Какво искате да направите с останалите задачи?",
|
||||
"challengeCompleted": "Това предизвикателство е приключило и победителят е <span class=\"badge\"><%= user %></span>! Какво искате да направите с останалите задачи?",
|
||||
"unsubChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но Вие сте се отписали от него. Какво искате да направите с останалите задачи?",
|
||||
"challenges": "Предизвикателства",
|
||||
"endDate": "Крайна дата",
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
"questEggVelociraptorText": "Велоцираптор",
|
||||
"questEggVelociraptorMountText": "Велоцираптор",
|
||||
"questEggVelociraptorAdjective": "умен",
|
||||
"eggNotes": "Намерете излюпваща отвара, която да излеете върху това яйце и от него ще се излюпи <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||
"eggNotes": "Намерете излюпваща отвара, която да излеете върху това яйце и от него ще се излюпи <%= eggAdjective %> <%= eggText %>.",
|
||||
"hatchingPotionBase": "Нормален цвят",
|
||||
"hatchingPotionWhite": "Бял цвят",
|
||||
"hatchingPotionDesert": "Пустинен цвят",
|
||||
@@ -211,7 +211,7 @@
|
||||
"hatchingPotionGlow": "Светещо в тъмното",
|
||||
"hatchingPotionFrost": "Скреж",
|
||||
"hatchingPotionIcySnow": "Леден сняг",
|
||||
"hatchingPotionNotes": "Излейте това върху яйце и от него ще се излюпи любимец с(ъс) <%= potText(locale) %>.",
|
||||
"hatchingPotionNotes": "Излейте това върху яйце и от него ще се излюпи любимец с(ъс) <%= potText %>.",
|
||||
"foodMeat": "Месо",
|
||||
"foodMeatThe": "Месото",
|
||||
"foodMeatA": "Месо",
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"messages": "Съобщения",
|
||||
"emptyMessagesLine1": "Нямате съобщения",
|
||||
"emptyMessagesLine2": "Можете да изпратите ново съобщение на потребител, като посетите профила им и докоснете бутона \"Съобщение\".",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> Ви изпрати съобщение",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%= user %></span> Ви изпрати съобщение",
|
||||
"letsgo": "Хойде!",
|
||||
"selected": "Избрано",
|
||||
"howManyToBuy": "Колко искате да купите?",
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"userId": "Потребителски идентификатор",
|
||||
"invite": "Покана",
|
||||
"leave": "Напускане",
|
||||
"invitedToParty": "Получихте покана за присъединяване към групата <span class=\"notification-bold\"><%- party %></span>",
|
||||
"invitedToPrivateGuild": "Получихте покана за присъединяване към частната гилдия <span class=\"notification-bold\"><%- guild %></span>",
|
||||
"invitedToPublicGuild": "Получихте покана за присъединяване към гилдията <span class=\"notification-bold-blue\"><%- guild %></span>",
|
||||
"invitedToParty": "Получихте покана за присъединяване към групата <span class=\"notification-bold\"><%= party %></span>",
|
||||
"invitedToPrivateGuild": "Получихте покана за присъединяване към частната гилдия <span class=\"notification-bold\"><%= guild %></span>",
|
||||
"invitedToPublicGuild": "Получихте покана за присъединяване към гилдията <span class=\"notification-bold-blue\"><%= guild %></span>",
|
||||
"invitationAcceptedHeader": "Поканата Ви беше приета",
|
||||
"invitationAcceptedBody": "<%= username %> прие поканата Ви да се присъедини към <%= groupName %>!",
|
||||
"systemMessage": "Системно съобщение",
|
||||
"newMsgGuild": "Има нови публикации в <span class=\"notification-bold-blue\"><%- name %></span>",
|
||||
"newMsgParty": "Има нови публикации в групата Ви — <span class=\"notification-bold-blue\"><%- name %></span>",
|
||||
"newMsgGuild": "Има нови публикации в <span class=\"notification-bold-blue\"><%= name %></span>",
|
||||
"newMsgParty": "Има нови публикации в групата Ви — <span class=\"notification-bold-blue\"><%= name %></span>",
|
||||
"chat": "Съобщения",
|
||||
"sendChat": "Изпращане на съобщението",
|
||||
"group": "Група",
|
||||
@@ -151,14 +151,14 @@
|
||||
"onlyGroupLeaderCanEditTasks": "Нямате право да управлявате задачите!",
|
||||
"onlyGroupTasksCanBeAssigned": "Само групови задачи могат да бъдат зададени",
|
||||
"assignedTo": "Назначена на",
|
||||
"assignedToUser": "Назначена на <strong><%- userName %></strong>",
|
||||
"assignedToUser": "Назначена на <strong><%= userName %></strong>",
|
||||
"assignedToMembers": "Назначена на <strong><%= userCount %> членове </strong>",
|
||||
"assignedToYouAndMembers": "Назначена на Вас и още <strong><%= userCount %> членове</strong>",
|
||||
"youAreAssigned": "Тази задача е назначена на Вас",
|
||||
"taskIsUnassigned": "Тази задача не е зададена на никого",
|
||||
"confirmUnClaim": "Наистина ли искате да оставите тази задача?",
|
||||
"confirmNeedsWork": "Наистина ли искате да отбележите, че тази задача се нуждае от още работа?",
|
||||
"userRequestsApproval": "<strong><%- userName %></strong> иска одобрение",
|
||||
"userRequestsApproval": "<strong><%= userName %></strong> иска одобрение",
|
||||
"userCountRequestsApproval": "<strong><%= userCount %> членове</strong> искат одобрение",
|
||||
"youAreRequestingApproval": "Вие искате одобрение",
|
||||
"chatPrivilegesRevoked": "Не можете да направите това, защото привилегиите Ви в чата са Ви били отнети. За детайли или запитване за връшане на привилегии, моля пратете email на нашия Обществен Оправител на admin@habitica.com или попитайте вашия родител или настойник да им прати email. Моля, напишете и потребителското си име в писмото. Ако модератор вече ви е казал че блокирането ви към чата е временно, няма нужда да пращате email.",
|
||||
@@ -168,9 +168,9 @@
|
||||
"claim": "Вземане на Задача",
|
||||
"removeClaim": "Отказване от задачата",
|
||||
"onlyGroupLeaderCanManageSubscription": "Само водачът на групата може да управлява абонамента ѝ",
|
||||
"yourTaskHasBeenApproved": "Задачата Ви <span class=\"notification-green notification-bold\"><%- taskText %></span>, беше одобрена.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> отбеляза, че задачата <span class=\"notification-bold\"><%- taskText %></span> се нуждае от още работа.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> помоли следната задача да бъде одобрена: <span class=\"notification-bold\"><%- taskName %></span>",
|
||||
"yourTaskHasBeenApproved": "Задачата Ви <span class=\"notification-green notification-bold\"><%= taskText %></span>, беше одобрена.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> отбеляза, че задачата <span class=\"notification-bold\"><%= taskText %></span> се нуждае от още работа.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> помоли следната задача да бъде одобрена: <span class=\"notification-bold\"><%= taskName %></span>",
|
||||
"approve": "Одобряване",
|
||||
"approveTask": "Одобряване на задачата",
|
||||
"needsWork": "Нуждае се от още работа",
|
||||
@@ -183,8 +183,8 @@
|
||||
"userIsClamingTask": "`<%= username %> пое:` <%= task %>",
|
||||
"approvalRequested": "Заявено е одобрение",
|
||||
"cantDeleteAssignedGroupTasks": "Не можете да изтриете груповите задачи, които са Ви разпределени.",
|
||||
"groupPlanUpgraded": "<strong><%- groupName %></strong> премина към групов план!",
|
||||
"groupPlanCreated": "Групата <strong><%- groupName %></strong> беше създадена!",
|
||||
"groupPlanUpgraded": "<strong><%= groupName %></strong> премина към групов план!",
|
||||
"groupPlanCreated": "Групата <strong><%= groupName %></strong> беше създадена!",
|
||||
"onlyGroupLeaderCanInviteToGroupPlan": "Само водачът на групата може да кани хора в група с абонамент.",
|
||||
"paymentDetails": "Подробности за разплащането",
|
||||
"aboutToJoinCancelledGroupPlan": "На път сте да се присъедините към група, чийто план е прекратен. НЯМА да получите безплатен абонамент.",
|
||||
@@ -325,8 +325,8 @@
|
||||
"PMDisabled": "Деактивиране на лични съобщения",
|
||||
"groupActivityNotificationTitle": "<%= user %> публикува в <%= group %>",
|
||||
"suggestedGroup": "Предложено, защото сте нови в Habitica.",
|
||||
"taskClaimed": "<%- userName %> взеха задачата <span class=\"notification-bold\"><%- taskText %></span>.",
|
||||
"youHaveBeenAssignedTask": "<%- managerName %> ви присвои задачата <span class=\"notification-bold\"><%- taskText %></span>.",
|
||||
"taskClaimed": "<%= userName %> взеха задачата <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"youHaveBeenAssignedTask": "<%= managerName %> ви присвои задачата <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"userWithUsernameOrUserIdNotFound": "Потребителското име или Потребителският Идентификатор не бяха намерени.",
|
||||
"usernameOrUserId": "Потребителско име или Потребителски Идентификатор",
|
||||
"sendGiftToWhom": "На кой бихте искали да пратите подарък?",
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
"paymentMethods": "Купуване чрез",
|
||||
"paymentSuccessful": "Плащането Ви беше успешно!",
|
||||
"paymentYouReceived": "Получихте:",
|
||||
"paymentYouSentGems": "Изпратихте на <strong><%- name %></strong>:",
|
||||
"paymentYouSentSubscription": "Изпратихте на <strong><%- name %></strong> <%= months %>-месечен абонамент за Хабитика.",
|
||||
"paymentYouSentGems": "Изпратихте на <strong><%= name %></strong>:",
|
||||
"paymentYouSentSubscription": "Изпратихте на <strong><%= name %></strong> <%= months %>-месечен абонамент за Хабитика.",
|
||||
"paymentSubBilling": "Абонаментът Ви ще бъде таксуван с <strong>$<%= amount %></strong> всеки <strong><%= months %> месеца</strong>.",
|
||||
"success": "Готово!",
|
||||
"classGear": "Снаряжение за класа",
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
"mountNotOwned": "Не притежавате този прево.",
|
||||
"feedPet": "Искате ли да дадете <%= text %> на <%= name %>?",
|
||||
"raisedPet": "Вие отгледахте <%= pet %>!",
|
||||
"petName": "<%= egg(locale) %> с(ъс) <%= potion(locale) %>",
|
||||
"mountName": "<%= mount(locale) %> с(ъс) <%= potion(locale) %>",
|
||||
"petName": "<%= egg %> с(ъс) <%= potion %>",
|
||||
"mountName": "<%= mount %> с(ъс) <%= potion %>",
|
||||
"keyToPets": "Ключ от зверилника за любимци",
|
||||
"keyToPetsDesc": "Освобождаване на всички стандартни любимци, за да можете да ги съберете отново. (Това не засяга любимците от мисии и редките любимци.)",
|
||||
"keyToMounts": "Ключ от зверилника за превози",
|
||||
|
||||
@@ -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,10 @@
|
||||
"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!"
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"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?",
|
||||
"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?",
|
||||
"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",
|
||||
"endDate": "Končí",
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
"questEggVelociraptorText": "Velociraptor",
|
||||
"questEggVelociraptorMountText": "Velociraptor",
|
||||
"questEggVelociraptorAdjective": "chytrý",
|
||||
"eggNotes": "Najdi líhnoucí lektvar, nalij ho na vejce a to se vylíhne v <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||
"eggNotes": "Najdi líhnoucí lektvar, nalij ho na vejce a to se vylíhne v <%= eggAdjective %> <%= eggText %>.",
|
||||
"hatchingPotionBase": "Základní",
|
||||
"hatchingPotionWhite": "Bílý",
|
||||
"hatchingPotionDesert": "Pouštní",
|
||||
@@ -211,7 +211,7 @@
|
||||
"hatchingPotionGlow": "Ve tmě svítící",
|
||||
"hatchingPotionFrost": "Zmrzlý",
|
||||
"hatchingPotionIcySnow": "Ledově Sněhový",
|
||||
"hatchingPotionNotes": "Nalij ho na vejce a vylíhne se ti <%= potText(locale) %> mazlíček.",
|
||||
"hatchingPotionNotes": "Nalij ho na vejce a vylíhne se ti <%= potText %> mazlíček.",
|
||||
"foodMeat": "Maso",
|
||||
"foodMeatThe": "Maso",
|
||||
"foodMeatA": "Maso",
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"messages": "Zprávy",
|
||||
"emptyMessagesLine1": "Nemáš žádné zprávy",
|
||||
"emptyMessagesLine2": "Novou zprávu uživateli/Česku můžeš poslat tak, že navštívíš jeho/její profil a klikneš na tlačítko “Zprávy”.",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> ti poslal/a zprávu",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%= user %></span> ti poslal/a zprávu",
|
||||
"letsgo": "Pojďmě!",
|
||||
"selected": "Vybrané",
|
||||
"howManyToBuy": "Kolik by jsi chtěl koupit?",
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"userId": "Uživatelské ID",
|
||||
"invite": "Pozvat",
|
||||
"leave": "Odejít",
|
||||
"invitedToParty": "Byl jsi pozván do družiny <span class=\"notification-bold\"><%- party %></span>",
|
||||
"invitedToPrivateGuild": "Byl jsi pozván do soukromého cechu <span class=\"notification-bold\"><%- guild %></span>",
|
||||
"invitedToPublicGuild": "Byl jsi pozván do cechu <span class=\"notification-bold-blue\"><%- guild %></span>",
|
||||
"invitedToParty": "Byl jsi pozván do družiny <span class=\"notification-bold\"><%= party %></span>",
|
||||
"invitedToPrivateGuild": "Byl jsi pozván do soukromého cechu <span class=\"notification-bold\"><%= guild %></span>",
|
||||
"invitedToPublicGuild": "Byl jsi pozván do cechu <span class=\"notification-bold-blue\"><%= guild %></span>",
|
||||
"invitationAcceptedHeader": "Tvá pozvánka byla přijata",
|
||||
"invitationAcceptedBody": "<%= username %> přijal tvoji pozvánku do <%= groupName %>!",
|
||||
"systemMessage": "Systémová zpráva",
|
||||
"newMsgGuild": "<span class=\"notification-bold-blue\"><%- name %></span> má nový příspěvek",
|
||||
"newMsgParty": "Tvá družina, <span class=\"notification-bold-blue\"><%- name %></span>, má nový příspěvek",
|
||||
"newMsgGuild": "<span class=\"notification-bold-blue\"><%= name %></span> má nový příspěvek",
|
||||
"newMsgParty": "Tvá družina, <span class=\"notification-bold-blue\"><%= name %></span>, má nový příspěvek",
|
||||
"chat": "Chat",
|
||||
"sendChat": "Poslat zprávu",
|
||||
"group": "Skupina",
|
||||
@@ -151,14 +151,14 @@
|
||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||
"assignedTo": "Přiřadit k",
|
||||
"assignedToUser": "Přiřazeno <strong><%- userName %></strong>",
|
||||
"assignedToUser": "Přiřazeno <strong><%= userName %></strong>",
|
||||
"assignedToMembers": "Přiřazeno <strong><%= userCount %> members</strong>",
|
||||
"assignedToYouAndMembers": "Přiřazeno vám a <strong><%= userCount %> members</strong>",
|
||||
"youAreAssigned": "Jsi přiřazen/a k tomuto úkolu",
|
||||
"taskIsUnassigned": "This task is unassigned",
|
||||
"confirmUnClaim": "Are you sure you want to unclaim this task?",
|
||||
"confirmNeedsWork": "Are you sure you want to mark this task as needing work?",
|
||||
"userRequestsApproval": "<strong><%- userName %></strong> požaduje schválení",
|
||||
"userRequestsApproval": "<strong><%= userName %></strong> požaduje schválení",
|
||||
"userCountRequestsApproval": "<strong><%= userCount %> members</strong> požadují schválení",
|
||||
"youAreRequestingApproval": "You are requesting approval",
|
||||
"chatPrivilegesRevoked": "Toto nelze provést, protože vaše oprávnění k chatu byla odstraněna. Chcete-li získat další informace nebo se zeptat, zda lze vaše oprávnění vrátit, pošlete e-mail našemu komunitnímu manažerovi na adrese admin@habitica.com nebo požádejte svého rodiče nebo zákonného zástupce o zaslání e-mailu. Do e-mailu uveďte prosím své @uživatelskéjméno. Pokud vám moderátor již řekl, že váš zákaz chatu je dočasný, nemusíte posílat e-maily.",
|
||||
@@ -168,9 +168,9 @@
|
||||
"claim": "Nárokovat úkol",
|
||||
"removeClaim": "Remove Claim",
|
||||
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
||||
"yourTaskHasBeenApproved": "Váš úkol <span class=\"notification-green notification-bold\"><%- taskText %></span> byl schválený.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> marked <span class=\"notification-bold\"><%- taskText %></span> as needing additional work.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> requests approval for <span class=\"notification-bold\"><%- taskName %></span>",
|
||||
"yourTaskHasBeenApproved": "Váš úkol <span class=\"notification-green notification-bold\"><%= taskText %></span> byl schválený.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
|
||||
"approve": "Approve",
|
||||
"approveTask": "Approve Task",
|
||||
"needsWork": "Needs Work",
|
||||
@@ -183,8 +183,8 @@
|
||||
"userIsClamingTask": "`<%= username %> has claimed:` <%= task %>",
|
||||
"approvalRequested": "Approval Requested",
|
||||
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
|
||||
"groupPlanUpgraded": "<strong><%- groupName %></strong> was upgraded to a Group Plan!",
|
||||
"groupPlanCreated": "<strong><%- groupName %></strong> was created!",
|
||||
"groupPlanUpgraded": "<strong><%= groupName %></strong> was upgraded to a Group Plan!",
|
||||
"groupPlanCreated": "<strong><%= groupName %></strong> was created!",
|
||||
"onlyGroupLeaderCanInviteToGroupPlan": "Only the group leader can invite users to a group with a subscription.",
|
||||
"paymentDetails": "Payment Details",
|
||||
"aboutToJoinCancelledGroupPlan": "You are about to join a group with a canceled plan. You will NOT receive a free subscription.",
|
||||
@@ -321,8 +321,8 @@
|
||||
"allAssignedCompletion": "All - Completes when all assigned users finish",
|
||||
"groupActivityNotificationTitle": "<%= user %> publikoval v <%= group %>",
|
||||
"suggestedGroup": "Navrženo, protože jste v Habitica nový/á.",
|
||||
"taskClaimed": "<%- userName %> nárokoval úkol <span class=\"notification-bold\"><%- taskText %></span>.",
|
||||
"youHaveBeenAssignedTask": "<%- managerName %> vám přidělil úkol <span class=\"notification-bold\"><%- taskText %></span>.",
|
||||
"taskClaimed": "<%= userName %> nárokoval úkol <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"youHaveBeenAssignedTask": "<%= managerName %> vám přidělil úkol <span class=\"notification-bold\"><%= taskText %></span>.",
|
||||
"pmReported": "Děkujeme za nahlášení této zprávy.",
|
||||
"newPartyPlaceholder": "Zadej jméno tvé družiny.",
|
||||
"userWithUsernameOrUserIdNotFound": "Uživatelské jméno nebo uživatelské ID nebylo nalezeno.",
|
||||
@@ -336,7 +336,7 @@
|
||||
"PMDisabled": "Zakaž soukromé zprávy",
|
||||
"unassigned": "Nepřiřazeno",
|
||||
"claimRewards": "Vyzvedni si odměnu",
|
||||
"assignedDateAndUser": "Přiřazeno uživatelem/kou <strong>@<%- username %></strong> dne <strong><%= date %></strong>",
|
||||
"assignedDateAndUser": "Přiřazeno uživatelem/kou <strong>@<%= username %></strong> dne <strong><%= date %></strong>",
|
||||
"assignedDateOnly": "Přiřazeno k <strong><%= date %></strong>",
|
||||
"managerNotes": "Poznámky manažera",
|
||||
"thisTaskApproved": "Tento úkol byl schválen",
|
||||
|
||||
@@ -82,8 +82,8 @@
|
||||
"paymentMethods": "Platební metody",
|
||||
"paymentSuccessful": "Tvá platba proběhla úspěšně!",
|
||||
"paymentYouReceived": "Obdržel jsi:",
|
||||
"paymentYouSentGems": "Poslal/a jsi <strong><%- name %></strong>:",
|
||||
"paymentYouSentSubscription": "Poslal/a jsi <strong><%- name %></strong> předplatné na <%= months %>-měsíce/ů v Habitica.",
|
||||
"paymentYouSentGems": "Poslal/a jsi <strong><%= name %></strong>:",
|
||||
"paymentYouSentSubscription": "Poslal/a jsi <strong><%= name %></strong> předplatné na <%= months %>-měsíce/ů v Habitica.",
|
||||
"paymentSubBilling": "Tvoje předplatné ve výši <strong>$<%= amount %></strong> bude účtovano každé/ých <strong><%= months %> měsíce/ů </strong>.",
|
||||
"success": "Úspěch!",
|
||||
"classGear": "Vybavení pro tvé povolání",
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
"mountNotOwned": "Nevlastníš toto jezdecké zvíře.",
|
||||
"feedPet": "Dát <%= text %> svému <%= name %>?",
|
||||
"raisedPet": "Vychoval jsi svého <%= pet %>!",
|
||||
"petName": "<%= potion(locale) %> <%= egg(locale) %>",
|
||||
"mountName": "<%= potion(locale) %> <%= mount(locale) %>",
|
||||
"petName": "<%= potion %> <%= egg %>",
|
||||
"mountName": "<%= potion %> <%= mount %>",
|
||||
"keyToPets": "Klíč ke Kotcům Mazlíčků",
|
||||
"keyToPetsDesc": "Propusť všechny své běžné mazlíčky abys je mohl sbírat znovu. (Ti vzácní a z výprav tím nebudou ovlivněni.)",
|
||||
"keyToMounts": "Klíč ke Kotcům Zvířat",
|
||||
|
||||
@@ -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í"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"keepIt": "Behold den",
|
||||
"removeIt": "Fjern den",
|
||||
"brokenChallenge": "Defekt udfordringslink: denne opgave var en del af en udfordring, men udfordringen (eller gruppen) er blevet fjernet. Hvad vil du gøre med de gruppeløse opgaver?",
|
||||
"challengeCompleted": "Denne udfordring er afsluttet, og vinderen blev <span class=\"badge\"><%- user %></span>! Hvad vil du gøre med de gruppeløse opgaver?",
|
||||
"challengeCompleted": "Denne udfordring er afsluttet, og vinderen blev <span class=\"badge\"><%= user %></span>! Hvad vil du gøre med de gruppeløse opgaver?",
|
||||
"unsubChallenge": "Defekt Udfordringslink: denne opgave var en del af en udfordring, som du ikke længere abonnerer på. Hvad vil du gøre med de gruppeløse opgaver?",
|
||||
"challenges": "Udfordringer",
|
||||
"endDate": "Afsluttes",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user