mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-20 19:31:05 -05:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 963b4133ec | |||
| 53999a5b80 | |||
| 4510c90e41 | |||
| 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 | |||
| 84208f612e | |||
| 57e06334c0 | |||
| be695d25b3 | |||
| 77ee83f467 | |||
| 86556e346b | |||
| 2007a872c6 | |||
| a8348038de | |||
| 87bcd69979 | |||
| 8f8e84d0c7 | |||
| 2c18cb00cc | |||
| daa0fd18c0 | |||
| 5c555cbf88 | |||
| 7379c7b230 | |||
| c055537c38 | |||
| 7559feec8e | |||
| 43808696a8 | |||
| 72fb41c7e0 | |||
| 3bf18e09ed | |||
| 407a901883 | |||
| 81a008906b | |||
| 992a978923 | |||
| a8062ad615 | |||
| 781a904583 | |||
| d87946d912 | |||
| 7456ff2def | |||
| e0af620b40 | |||
| bb295551b5 | |||
| fce400f323 | |||
| c0ffb8b968 | |||
| 72539f9ba3 | |||
| dabd466719 | |||
| 8bf2304330 | |||
| 6937dc4e4e | |||
| 2917955ef0 | |||
| 55d13e44d4 | |||
| 90096f995f | |||
| 5c74c2b914 | |||
| 1f1a44e16f | |||
| a275109a3e | |||
| c65457690b | |||
| f740f12b97 | |||
| 9fd0bfae46 | |||
| bee23efbef | |||
| a504b18ce4 | |||
| f556b102c6 | |||
| ac62de7bd8 | |||
| 5ff3cc35a6 | |||
| 215e5e1c40 | |||
| 02ca96ea51 | |||
| e70ae4e9aa | |||
| e2bf8ae493 | |||
| 931a70a797 | |||
| e2d2a05315 | |||
| be041f734d | |||
| c430d2279c |
@@ -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
-2
@@ -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",
|
||||
@@ -90,7 +90,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: 98642906db...359153997e
@@ -73,7 +73,7 @@ export default async function processUsers () {
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Generated
+426
-605
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.41.4",
|
||||
"version": "5.44.3",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -40,6 +40,7 @@
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
@@ -48,10 +49,12 @@
|
||||
"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",
|
||||
@@ -100,13 +103,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 ubuntu2204 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2204 --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 +128,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"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
} from '../../../../../website/server/models/group';
|
||||
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
|
||||
import { MAX_MESSAGE_LENGTH, CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
|
||||
describe('POST /chat', () => {
|
||||
@@ -80,17 +80,20 @@ describe('POST /chat', () => {
|
||||
member.updateOne({ 'flags.chatRevoked': false });
|
||||
});
|
||||
|
||||
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
|
||||
it('errors when chat privileges are revoked when sending a message to a private guild', async () => {
|
||||
await member.updateOne({
|
||||
'flags.chatRevoked': true,
|
||||
});
|
||||
|
||||
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
await expect(member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not error when chat privileges are revoked when sending a message to a party', async () => {
|
||||
it('errors when chat privileges are revoked when sending a message to a party', async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
@@ -106,9 +109,12 @@ describe('POST /chat', () => {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
await expect(privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,7 +129,7 @@ describe('POST /chat', () => {
|
||||
member.updateOne({ 'flags.chatShadowMuted': false });
|
||||
});
|
||||
|
||||
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
|
||||
it('creates a chat with flagCount set when sending a message to a private guild', async () => {
|
||||
await member.updateOne({
|
||||
'flags.chatShadowMuted': true,
|
||||
});
|
||||
@@ -131,10 +137,10 @@ describe('POST /chat', () => {
|
||||
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.flagCount).to.eql(0);
|
||||
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
|
||||
});
|
||||
|
||||
it('creates a chat with zero flagCount when sending a message to a party', async () => {
|
||||
it('creates a chat with flagCount set when sending a message to a party', async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
@@ -153,7 +159,7 @@ describe('POST /chat', () => {
|
||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.flagCount).to.eql(0);
|
||||
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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', () => {});
|
||||
@@ -61,6 +61,24 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
usernames: [userToInvite.auth.local.lowerCaseUsername],
|
||||
});
|
||||
expect(response).to.be.an('Array');
|
||||
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
|
||||
expect(response[0]._id).to.be.a('String');
|
||||
expect(response[0].id).to.eql(group._id);
|
||||
expect(response[0].name).to.eql(groupName);
|
||||
expect(response[0].inviter).to.eql(inviter._id);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
|
||||
});
|
||||
|
||||
it('invites a user to a group by username', async () => {
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
@@ -209,6 +227,24 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [userToInvite._id],
|
||||
});
|
||||
expect(response).to.be.an('Array');
|
||||
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
|
||||
expect(response[0]._id).to.be.a('String');
|
||||
expect(response[0].id).to.eql(group._id);
|
||||
expect(response[0].name).to.eql(groupName);
|
||||
expect(response[0].inviter).to.eql(inviter._id);
|
||||
|
||||
await expect(userToInvite.get('/user'))
|
||||
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
|
||||
});
|
||||
|
||||
it('invites a user to a group by uuid', async () => {
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
@@ -281,6 +317,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending invite when inviter is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const res = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
emails: [testInvite],
|
||||
inviter: 'inviter name',
|
||||
});
|
||||
|
||||
const updatedUser = await inviterMuted.sync();
|
||||
|
||||
expect(res).to.exist;
|
||||
expect(updatedUser.invitesSent).to.eql(1);
|
||||
});
|
||||
|
||||
it('returns an error when invite is missing an email', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
emails: [{ name: 'test' }],
|
||||
@@ -405,6 +454,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('fakes sending an invite if user is shadow muted', async () => {
|
||||
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
|
||||
const newUser = await generateUser();
|
||||
const invite = await inviterMuted.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [newUser._id],
|
||||
emails: [{ name: 'test', email: 'test@habitica.com' }],
|
||||
});
|
||||
const invitedUser = await newUser.get('/user');
|
||||
|
||||
expect(invitedUser.invitations.parties[0]).to.not.exist;
|
||||
expect(invite).to.exist;
|
||||
});
|
||||
|
||||
it('invites users to a group by uuid and email', async () => {
|
||||
const newUser = await generateUser();
|
||||
const invite = await inviter.post(`/groups/${group._id}/invite`, {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user has blocked the sender', async () => {
|
||||
it('returns error when recipient has blocked the sender', async () => {
|
||||
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
|
||||
|
||||
await expect(userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -56,7 +56,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when sender has blocked to user', async () => {
|
||||
it('returns error when sender has blocked recipient', async () => {
|
||||
const receiver = await generateUser();
|
||||
const sender = await generateUser({ 'inbox.blocks': [receiver._id] });
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('POST /members/send-private-message', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when to user has opted out of messaging', async () => {
|
||||
it('returns error when recipient has opted out of messaging', async () => {
|
||||
const receiver = await generateUser({ 'inbox.optOut': true });
|
||||
|
||||
await expect(userToSendMessage.post('/members/send-private-message', {
|
||||
@@ -174,7 +174,7 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(notification.data.excerpt).to.equal(messageExcerpt);
|
||||
});
|
||||
|
||||
it('allows admin to send when sender has blocked the admin', async () => {
|
||||
it('allows admin to send when recipient has blocked the admin', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
@@ -202,7 +202,7 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('allows admin to send when to user has opted out of messaging', async () => {
|
||||
it('allows admin to send when recipient has opted out of messaging', async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'permissions.moderator': true,
|
||||
});
|
||||
@@ -229,4 +229,58 @@ describe('POST /members/send-private-message', () => {
|
||||
expect(sendersMessageInReceiversInbox).to.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
describe('sender is shadow muted', () => {
|
||||
beforeEach(async () => {
|
||||
userToSendMessage = await generateUser({
|
||||
'flags.chatShadowMuted': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not save the message in the receiver inbox', async () => {
|
||||
const receiver = await generateUser();
|
||||
|
||||
const response = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: receiver._id,
|
||||
});
|
||||
|
||||
expect(response.message.uuid).to.equal(receiver._id);
|
||||
|
||||
const updatedReceiver = await receiver.get('/user');
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInReceiversInbox = _.find(
|
||||
updatedReceiver.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
updatedSender.inbox.messages,
|
||||
message => message.uuid === receiver._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
expect(sendersMessageInReceiversInbox).to.not.exist;
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
});
|
||||
|
||||
it('does not save the message message twice if recipient is sender', async () => {
|
||||
const response = await userToSendMessage.post('/members/send-private-message', {
|
||||
message: messageToSend,
|
||||
toUserId: userToSendMessage._id,
|
||||
});
|
||||
|
||||
expect(response.message.uuid).to.equal(userToSendMessage._id);
|
||||
|
||||
const updatedSender = await userToSendMessage.get('/user');
|
||||
|
||||
const sendersMessageInSendersInbox = _.find(
|
||||
updatedSender.inbox.messages,
|
||||
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
|
||||
);
|
||||
|
||||
expect(sendersMessageInSendersInbox).to.exist;
|
||||
expect(Object.keys(updatedSender.inbox.messages).length).to.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
+4968
-2726
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",
|
||||
@@ -33,6 +34,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1055,6 +1055,16 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_elegant_palace {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elegant_palace.png');
|
||||
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;
|
||||
@@ -1751,6 +1761,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_nighttime_street_with_shops {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_nighttime_street_with_shops.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_ocean_sunrise {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_ocean_sunrise.png');
|
||||
width: 141px;
|
||||
@@ -1921,6 +1936,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;
|
||||
@@ -2417,6 +2437,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;
|
||||
@@ -2437,6 +2462,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_winter_desert_with_saguaros {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_desert_with_saguaros.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_winter_fireworks {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_fireworks.png');
|
||||
width: 141px;
|
||||
@@ -29455,6 +29485,11 @@
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
.back_armoire_harpsichord {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_armoire_harpsichord.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.body_armoire_clownsBowtie {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_armoire_clownsBowtie.png');
|
||||
width: 114px;
|
||||
@@ -29780,6 +29815,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;
|
||||
@@ -29845,6 +29885,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_loneCowpokeOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_loneCowpokeOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_lunarArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lunarArmor.png');
|
||||
width: 90px;
|
||||
@@ -30050,6 +30095,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;
|
||||
@@ -30360,6 +30410,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;
|
||||
@@ -30480,6 +30535,11 @@
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.head_armoire_loneCowpokeHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_loneCowpokeHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_lunarCrown {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lunarCrown.png');
|
||||
width: 90px;
|
||||
@@ -30675,6 +30735,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;
|
||||
@@ -30790,6 +30855,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_doubleBass {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_doubleBass.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_dragonTamerShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dragonTamerShield.png');
|
||||
width: 90px;
|
||||
@@ -30995,6 +31065,11 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_prettyPinkGiftBox {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_prettyPinkGiftBox.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_ramHornShield {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_ramHornShield.png');
|
||||
width: 90px;
|
||||
@@ -31080,6 +31155,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;
|
||||
@@ -31130,6 +31210,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;
|
||||
@@ -31400,6 +31485,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;
|
||||
@@ -31465,6 +31555,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_loneCowpokeOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_loneCowpokeOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_lunarArmor {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lunarArmor.png');
|
||||
width: 90px;
|
||||
@@ -31670,6 +31765,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;
|
||||
@@ -31760,6 +31860,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_bambooFlute {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_bambooFlute.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_barristerGavel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_barristerGavel.png');
|
||||
width: 90px;
|
||||
@@ -32170,6 +32275,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_prettyPinkParasol {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_prettyPinkParasol.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pushBroom {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
|
||||
width: 114px;
|
||||
@@ -34060,6 +34170,81 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202601 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202601.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.back_mystery_202602 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202602.png');
|
||||
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;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202602 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202602.png');
|
||||
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;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202601 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202601.png');
|
||||
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;
|
||||
@@ -36180,6 +36365,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;
|
||||
@@ -36500,6 +36705,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;
|
||||
@@ -36685,6 +36910,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;
|
||||
@@ -36920,6 +37160,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;
|
||||
@@ -37160,6 +37420,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;
|
||||
@@ -38640,6 +38920,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -38935,6 +39235,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -39115,6 +39435,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -39355,6 +39690,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.slim_armor_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_yeti.png');
|
||||
width: 90px;
|
||||
@@ -39595,6 +39950,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_winter2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Rogue.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_special_winter2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_yeti {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_yeti.png');
|
||||
width: 90px;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -311,7 +311,7 @@
|
||||
<input
|
||||
id="passwordInput"
|
||||
v-model="password"
|
||||
class="form-control input-with-error"
|
||||
class="form-control dark input-with-error"
|
||||
type="password"
|
||||
:placeholder="$t('password')"
|
||||
:class="{'input-invalid': passwordInvalid, 'input-valid': passwordValid}"
|
||||
@@ -323,7 +323,7 @@
|
||||
{{ $t('minPasswordLength') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-4">
|
||||
<label
|
||||
v-once
|
||||
for="confirmPasswordInput"
|
||||
@@ -331,7 +331,7 @@
|
||||
<input
|
||||
id="confirmPasswordInput"
|
||||
v-model="passwordConfirm"
|
||||
class="form-control input-with-error"
|
||||
class="form-control dark input-with-error"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPasswordPlaceholder')"
|
||||
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
|
||||
@@ -344,13 +344,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="btn btn-info"
|
||||
:enabled="!resetPasswordSetNewOneData.hasError"
|
||||
<button
|
||||
class="btn btn-info w-100"
|
||||
:disabled="!password || !passwordConfirm
|
||||
|| password !== passwordConfirm || resetPasswordSetNewOneData.hasError"
|
||||
@click="resetPasswordSetNewOneLink()"
|
||||
>
|
||||
{{ $t('setNewPass') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -672,7 +673,7 @@ export default {
|
||||
|
||||
this.login();
|
||||
},
|
||||
async forgotPasswordLink () {
|
||||
forgotPasswordLink: debounce(async function forgotPassLink () {
|
||||
if (!this.username) {
|
||||
window.alert(this.$t('missingEmail')); // eslint-disable-line no-alert
|
||||
return;
|
||||
@@ -683,7 +684,7 @@ export default {
|
||||
});
|
||||
|
||||
window.alert(this.$t('newPassSent')); // eslint-disable-line no-alert
|
||||
},
|
||||
}, 500),
|
||||
async resetPasswordSetNewOneLink () {
|
||||
if (!this.password) {
|
||||
window.alert(this.$t('missingNewPassword')); // eslint-disable-line no-alert
|
||||
|
||||
@@ -52,17 +52,21 @@
|
||||
<div
|
||||
v-if="!group.purchased.plan.dateTerminated
|
||||
&& group.purchased.plan.paymentMethod === 'Stripe'"
|
||||
class="btn btn-primary"
|
||||
class="btn btn-primary mb-3"
|
||||
@click="redirectToStripeEdit({groupId: group.id})"
|
||||
>
|
||||
{{ $t('subUpdateCard') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="!group.purchased.plan.dateTerminated"
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="cancelSubscriptionConfirm({group: group})"
|
||||
>
|
||||
{{ $t('cancelGroupSub') }}
|
||||
<div v-if="!group.purchased.plan.dateTerminated">
|
||||
<div class="small gray-50 mb-3" v-once>
|
||||
{{ $t('groupPlanBillingFYIShort') }}
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-sm btn-danger"
|
||||
@click="cancelSubscriptionConfirm({group: group})"
|
||||
>
|
||||
{{ $t('cancelGroupSub') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -218,13 +218,19 @@
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
// somehow the browser felt like setting this 398px instead
|
||||
// now its fixed to 400 :)
|
||||
width: 400px;
|
||||
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
@media (max-width: 589px) {
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.quest-col {
|
||||
::v-deep {
|
||||
.item-wrapper {
|
||||
@@ -251,6 +257,28 @@
|
||||
::v-deep & {
|
||||
.modal-dialog {
|
||||
width: 448px !important;
|
||||
max-width: calc(100vw - 20px);
|
||||
margin: 0.5rem auto;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
width: 100% !important;
|
||||
margin: 0.25rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="banner d-flex align-items-center justify-content-between py-3 px-4"
|
||||
id="privacy-banner"
|
||||
v-if="!hidden"
|
||||
id="privacy-banner"
|
||||
class="banner d-flex align-items-center justify-content-between py-3 px-4"
|
||||
>
|
||||
<p
|
||||
class="mr-3 mb-0"
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
>
|
||||
<div
|
||||
class="close-x"
|
||||
@click="remove()"
|
||||
@click.stop="remove()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon svg-close"
|
||||
@@ -140,7 +140,7 @@ export default {
|
||||
methods: {
|
||||
remove () {
|
||||
if (this.eventKey) {
|
||||
window.sessionStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
|
||||
window.localStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
|
||||
}
|
||||
this.$emit('notification-removed');
|
||||
},
|
||||
|
||||
@@ -318,7 +318,7 @@ export default {
|
||||
shouldShowG1g1 () {
|
||||
if (!this.currentG1g1Event) return false;
|
||||
const eventKey = this.g1g1EventKey;
|
||||
if (eventKey && window.sessionStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
|
||||
if (eventKey && window.localStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
|
||||
return false;
|
||||
}
|
||||
return !this.g1g1Hidden;
|
||||
|
||||
@@ -491,6 +491,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
mapProfileLinksToModal () {
|
||||
if (!this.$refs?.markdownContainer) {
|
||||
return;
|
||||
}
|
||||
const links = this.$refs.markdownContainer.getElementsByTagName('a');
|
||||
for (let i = 0; i < links.length; i += 1) {
|
||||
let link = links[i].pathname;
|
||||
|
||||
@@ -328,6 +328,8 @@ export default {
|
||||
alreadyReadNotification,
|
||||
nextCron: null,
|
||||
handledNotifications,
|
||||
isInitialLoadComplete: false,
|
||||
pendingRebirthNotification: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -453,6 +455,18 @@ export default {
|
||||
|
||||
return this.runYesterDailies();
|
||||
},
|
||||
async showPendingRebirthModal () {
|
||||
if (this.pendingRebirthNotification) {
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
|
||||
await axios.post('/api/v4/notifications/read', {
|
||||
notificationIds: [this.pendingRebirthNotification.id],
|
||||
});
|
||||
|
||||
this.pendingRebirthNotification = null;
|
||||
}
|
||||
},
|
||||
showDeathModal () {
|
||||
this.playSound('Death');
|
||||
this.$root.$emit('bv::show::modal', 'death');
|
||||
@@ -661,6 +675,18 @@ export default {
|
||||
this.showLevelUpNotifications(this.user.stats.lvl);
|
||||
}
|
||||
this.handleUserNotifications(this.user.notifications);
|
||||
|
||||
this.isInitialLoadComplete = true;
|
||||
|
||||
const hasRebirthConfirmationFlag = localStorage.getItem('show-rebirth-confirmation') === 'true';
|
||||
|
||||
if (hasRebirthConfirmationFlag) {
|
||||
localStorage.removeItem('show-rebirth-confirmation');
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
} else {
|
||||
this.showPendingRebirthModal();
|
||||
}
|
||||
},
|
||||
async handleUserNotifications (after) {
|
||||
if (this.$store.state.isRunningYesterdailies) return;
|
||||
@@ -700,8 +726,15 @@ export default {
|
||||
this.$root.$emit('habitica:won-challenge', notification);
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
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');
|
||||
}
|
||||
break;
|
||||
case 'STREAK_ACHIEVEMENT':
|
||||
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
|
||||
z-index: 9;
|
||||
height: 3rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 683px) {
|
||||
height: auto;
|
||||
min-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@@ -23,6 +29,19 @@
|
||||
padding: 0.75rem;
|
||||
|
||||
color: $gray-50;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 683px) {
|
||||
padding: 0.5rem;
|
||||
font-size: 13px;
|
||||
flex: 1 1 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
padding: 0.5rem 0.4rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $purple-300;
|
||||
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
privacyConsent: true,
|
||||
privacyConsent: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
v-if="paymentMethodLogo.icon"
|
||||
class="svg svg-icon mb-4"
|
||||
:class="paymentMethodLogo.class"
|
||||
v-html="paymentMethodLogo.icon"
|
||||
@@ -205,6 +206,13 @@
|
||||
<div>{{ $t('subUpdateCard') }}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
v-if="!hasGroupPlan"
|
||||
class="small text-center mb-4"
|
||||
>
|
||||
{{ $t('subscriptionBillingFYIShort') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="purchasedPlanExtraMonthsDetails.months > 0"
|
||||
class="extra-months green-10 py-2 px-3 mb-4"
|
||||
@@ -407,6 +415,13 @@
|
||||
<div class="purple-bar my-auto"></div>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center mt-3">
|
||||
<div
|
||||
v-once
|
||||
v-if="!hasSubscription"
|
||||
class="small gray-100 w-50 text-center mb-5"
|
||||
>
|
||||
{{ $t('subscriptionBillingFYI') }}
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gift-box mb-2"
|
||||
@@ -631,7 +646,7 @@
|
||||
background-color: $purple-400;
|
||||
height: 1px;
|
||||
width: 50%;
|
||||
max-width: 432px;
|
||||
max-width: 417px;
|
||||
}
|
||||
|
||||
.purple-gradient {
|
||||
@@ -654,6 +669,12 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
line-height: 1.67;
|
||||
max-width: 874px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
border-radius: 8px;
|
||||
width: 192px;
|
||||
|
||||
@@ -269,12 +269,23 @@
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
max-width: calc(100vw - 20px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-dialog {
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
|
||||
.badge-pin {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
@@ -346,7 +357,23 @@
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
width: 448px;
|
||||
width: 100%;
|
||||
max-width: 448px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
@@ -564,7 +591,7 @@
|
||||
|
||||
.limitedTime {
|
||||
height: 32px;
|
||||
width: 446px;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
margin: 24px 0 0 0;
|
||||
background-color: $purple-300;
|
||||
@@ -829,10 +856,17 @@ export default {
|
||||
- ownedMounts
|
||||
- ownedItems;
|
||||
|
||||
if (
|
||||
petsRemaining < 0
|
||||
&& !window.confirm(this.$t('purchasePetItemConfirm', { itemText: this.item.text })) // eslint-disable-line no-alert
|
||||
) return;
|
||||
if (petsRemaining < 0) {
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:purchase-confirm', {
|
||||
message: this.$t('purchasePetItemConfirm', { itemText: this.item.text }),
|
||||
currency: this.item.currency,
|
||||
cost: this.item.value * this.selectedAmountToBuy,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.item.purchaseType === 'customization') {
|
||||
@@ -844,15 +878,23 @@ export default {
|
||||
this.purchased(this.item.text);
|
||||
} else {
|
||||
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
|
||||
if (
|
||||
shouldConfirmPurchase
|
||||
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
|
||||
) {
|
||||
return;
|
||||
if (shouldConfirmPurchase) {
|
||||
const confirmed = await this.confirmPurchase(
|
||||
this.item.currency,
|
||||
this.item.value * this.selectedAmountToBuy,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.genericPurchase) {
|
||||
if (this.item.key === 'rebirth_orb') {
|
||||
localStorage.setItem('show-rebirth-confirmation', 'true');
|
||||
}
|
||||
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
await this.purchased(this.item.text);
|
||||
if (this.item.key !== 'rebirth_orb') {
|
||||
await this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,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;
|
||||
|
||||
@@ -111,6 +111,22 @@
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
max-width: calc(100vw - 20px);
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="purchase-confirm-modal"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
modal-class="purchase-confirm-modal"
|
||||
centered
|
||||
>
|
||||
<div class="modal-content-wrapper">
|
||||
<div class="top-bar"></div>
|
||||
<div class="modal-body-content">
|
||||
<div
|
||||
class="currency-chip"
|
||||
:class="currency"
|
||||
>
|
||||
<span
|
||||
class="svg-icon icon-24"
|
||||
v-html="icons[currency]"
|
||||
></span>
|
||||
<span class="cost-value">{{ cost }}</span>
|
||||
</div>
|
||||
<h2 class="modal-title">
|
||||
{{ $t('confirmPurchase') }}
|
||||
</h2>
|
||||
<p class="modal-subtitle">
|
||||
{{ confirmationMessage }}
|
||||
</p>
|
||||
<div class="button-wrapper">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="confirm()"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn-cancel"
|
||||
@click="cancel()"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import svgGem from '@/assets/svg/gem.svg?raw';
|
||||
import svgHourglass from '@/assets/svg/hourglass.svg?raw';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
confirmationMessage: '',
|
||||
currency: 'gems',
|
||||
cost: 0,
|
||||
resolveCallback: null,
|
||||
icons: Object.freeze({
|
||||
gems: svgGem,
|
||||
hourglasses: svgHourglass,
|
||||
}),
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:purchase-confirm', config => {
|
||||
this.confirmationMessage = config.message;
|
||||
this.currency = config.currency || 'gems';
|
||||
this.cost = config.cost || 0;
|
||||
this.resolveCallback = config.resolve;
|
||||
this.$root.$emit('bv::show::modal', 'purchase-confirm-modal');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:purchase-confirm');
|
||||
},
|
||||
methods: {
|
||||
confirm () {
|
||||
if (this.resolveCallback) {
|
||||
this.resolveCallback(true);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
cancel () {
|
||||
if (this.resolveCallback) {
|
||||
this.resolveCallback(false);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'purchase-confirm-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
::v-deep .purchase-confirm-modal {
|
||||
.modal-dialog {
|
||||
max-width: 330px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 8px;
|
||||
background-color: $purple-300;
|
||||
}
|
||||
|
||||
.modal-body-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.currency-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 40px;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
|
||||
&.gems {
|
||||
color: $gems-color;
|
||||
background-color: rgba($green-10, 0.15);
|
||||
}
|
||||
|
||||
&.hourglasses {
|
||||
color: $hourglass-color;
|
||||
background-color: rgba($blue-10, 0.15);
|
||||
}
|
||||
|
||||
.icon-24 {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
color: $purple-300;
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $purple-300;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -163,8 +163,38 @@
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin-top: 8%;
|
||||
width: 448px !important;
|
||||
max-width: calc(100vw - 20px);
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100% !important;
|
||||
margin: 3rem auto 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
margin: 2.5rem auto 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-dialog {
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
|
||||
.badge-pin {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -485,8 +515,12 @@ export default {
|
||||
this.selectedAmountToBuy = 1;
|
||||
this.$emit('change', $event);
|
||||
},
|
||||
buyItem () {
|
||||
if (!this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)) {
|
||||
async buyItem () {
|
||||
const confirmed = await this.confirmPurchase(
|
||||
this.item.currency,
|
||||
this.item.value * this.selectedAmountToBuy,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
this.makeGenericPurchase(this.item, 'buyQuestModal', this.selectedAmountToBuy);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<p class="gray-200">
|
||||
{{ $t('billedMonthly') }}
|
||||
</p>
|
||||
<small class="gray-200">
|
||||
{{ $t('groupPlanBillingFYI') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="top-right"></div>
|
||||
<div class="d-flex justify-content-between align-items-middle w-100 gap-72 mb-100">
|
||||
@@ -114,6 +117,9 @@
|
||||
<p class="gray-200">
|
||||
{{ $t('billedMonthly') }}
|
||||
</p>
|
||||
<small class="gray-200">
|
||||
{{ $t('groupPlanBillingFYI') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="bot-right"></div>
|
||||
</div>
|
||||
@@ -174,6 +180,11 @@
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 12px;
|
||||
line-height: 1.67;
|
||||
}
|
||||
|
||||
// Major layout elements
|
||||
|
||||
.bottom-banner {
|
||||
|
||||
@@ -11,12 +11,12 @@
|
||||
<privacy-banner
|
||||
class="privacy-banner"
|
||||
/>
|
||||
<div class="bg-purple-300 white">
|
||||
<div class="bg-purple-300 white pt-5">
|
||||
<div>
|
||||
<div
|
||||
id="intro-signup"
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex justify-content-center pb-5 mb-5">
|
||||
<div class="w-33 mr-5 mt-5">
|
||||
<img
|
||||
src="@/assets/images/home/home-main@3x.png"
|
||||
|
||||
@@ -64,9 +64,11 @@
|
||||
<li>sexual orientation; and</li>
|
||||
<li>information collected from a known child.</li>
|
||||
</ul>
|
||||
<p><strong>
|
||||
NOTE: Please do not provide us “sensitive personal information” or “sensitive personal data”, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
|
||||
</strong></p>
|
||||
<p>
|
||||
<strong>
|
||||
NOTE: Please do not provide us “sensitive personal information” or “sensitive personal data”, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
|
||||
</strong>
|
||||
</p>
|
||||
<h3 id="section_1_1">
|
||||
1.1 Information You Provide Directly
|
||||
</h3>
|
||||
@@ -617,7 +619,7 @@
|
||||
7. General Audience Services
|
||||
</h2>
|
||||
<p>
|
||||
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their children’s Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>, and we will delete that information from our databases.
|
||||
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their children’s Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
|
||||
</p>
|
||||
|
||||
<h2 id="section_8">
|
||||
@@ -708,7 +710,7 @@
|
||||
|
||||
<p><strong><u>Nevada Residents</u></strong></p>
|
||||
<p>
|
||||
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
|
||||
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>.
|
||||
</p>
|
||||
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
|
||||
<p>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<router-view />
|
||||
</div>
|
||||
<div
|
||||
id="bottom-background"
|
||||
v-if="loginFlow"
|
||||
id="bottom-background"
|
||||
class="bg-purple-300"
|
||||
>
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
@@ -31,7 +31,10 @@
|
||||
id="bottom-wrap"
|
||||
class="purple-4"
|
||||
>
|
||||
<div id="bottom-background" v-if="!loginFlow">
|
||||
<div
|
||||
v-if="!loginFlow"
|
||||
id="bottom-background"
|
||||
>
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
<div class="midground_foreground_extended2"></div>
|
||||
</div>
|
||||
@@ -104,9 +107,10 @@
|
||||
footer, footer a {
|
||||
background: transparent;
|
||||
color: $purple-500;
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -117,10 +121,6 @@
|
||||
border-top-color: $purple-100;
|
||||
}
|
||||
|
||||
.donate-text {
|
||||
color: $purple-500;
|
||||
}
|
||||
|
||||
.logo {
|
||||
color: $purple-300;
|
||||
}
|
||||
@@ -129,42 +129,27 @@
|
||||
color: $purple-500;
|
||||
}
|
||||
|
||||
.social .d-flex:hover {
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
svg {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.social-circle {
|
||||
background: $purple-50;
|
||||
color: $purple-500;
|
||||
|
||||
.instagram svg {
|
||||
svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.bluesky svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.facebook svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.tumblr svg {
|
||||
background-color: $purple-50;
|
||||
fill: $purple-500;
|
||||
&:hover {
|
||||
fill: $white;
|
||||
}
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-contribute {
|
||||
background: $white;
|
||||
box-shadow: none;
|
||||
@@ -274,7 +259,8 @@ export default {
|
||||
return 'purple-footer';
|
||||
},
|
||||
loginFlow () {
|
||||
return ['login', 'register', 'username'].indexOf(this.$route.name) !== -1;
|
||||
const loginRoutes = ['forgotPassword', 'login', 'register', 'resetPassword', 'username'];
|
||||
return loginRoutes.indexOf(this.$route.name) !== -1;
|
||||
},
|
||||
showContentWrap () {
|
||||
return this.$route.name !== 'news';
|
||||
|
||||
@@ -158,7 +158,7 @@
|
||||
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
|
||||
</p>
|
||||
<p>
|
||||
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href='mailto:admin@habitica.com'>ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
|
||||
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
|
||||
</p>
|
||||
<p>
|
||||
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
|
||||
|
||||
@@ -1,65 +1,45 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="broken-task-modal"
|
||||
title="Broken Challenge"
|
||||
size="sm"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
modal-class="broken-task-confirm-modal"
|
||||
centered
|
||||
>
|
||||
<div
|
||||
v-if="brokenChallengeTask && brokenChallengeTask.challenge"
|
||||
class="modal-body"
|
||||
class="modal-content-wrapper"
|
||||
>
|
||||
<div
|
||||
v-if="brokenChallengeTask.challenge.broken === 'TASK_DELETED'
|
||||
|| brokenChallengeTask.challenge.broken === 'CHALLENGE_TASK_NOT_FOUND'"
|
||||
>
|
||||
<h2>{{ $t('brokenTask') }}</h2>
|
||||
<div>
|
||||
<div class="top-bar"></div>
|
||||
<div class="modal-body-content">
|
||||
<div
|
||||
class="icon-wrapper"
|
||||
v-html="icons.alertIcon"
|
||||
></div>
|
||||
<h2 class="modal-title">
|
||||
{{ modalTitle }}
|
||||
</h2>
|
||||
<p class="modal-subtitle">
|
||||
{{ modalSubtitle }}
|
||||
</p>
|
||||
<div class="button-wrapper">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="unlink('keep')"
|
||||
@click="keepAction()"
|
||||
>
|
||||
{{ $t('keepIt') }}
|
||||
{{ keepButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="removeTask(obj)"
|
||||
@click="removeAction()"
|
||||
>
|
||||
{{ $t('removeIt') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_DELETED'">
|
||||
<h2>{{ $t('brokenChallenge') }}</h2>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="unlink('keep-all')"
|
||||
>
|
||||
{{ $t('keepTasks') }}
|
||||
{{ removeButtonText }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="unlink('remove-all')"
|
||||
class="btn-cancel"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('removeTasks') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_CLOSED'">
|
||||
<h2 v-html="$t('challengeCompleted', {user: brokenChallengeTask.challenge.winner})"></h2>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="unlink('keep-all')"
|
||||
>
|
||||
{{ $t('keepTasks') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="unlink('remove-all')"
|
||||
>
|
||||
{{ $t('removeTasks') }}
|
||||
{{ $t('cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,23 +47,175 @@
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.modal-body {
|
||||
padding-bottom: 2em;
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
::v-deep .broken-task-confirm-modal {
|
||||
.modal-dialog {
|
||||
max-width: 330px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 8px;
|
||||
background-color: $maroon-100;
|
||||
}
|
||||
|
||||
.modal-body-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
margin-top: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
::v-deep svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
path {
|
||||
fill: #DE3F3F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
color: $maroon-100;
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $purple-300;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapActions } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
data () {
|
||||
return {
|
||||
brokenChallengeTask: {},
|
||||
icons: Object.freeze({
|
||||
alertIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
brokenType () {
|
||||
return this.brokenChallengeTask.challenge?.broken;
|
||||
},
|
||||
isSingleTask () {
|
||||
return this.brokenType === 'TASK_DELETED'
|
||||
|| this.brokenType === 'CHALLENGE_TASK_NOT_FOUND';
|
||||
},
|
||||
brokenChallengeTaskCount () {
|
||||
if (!this.brokenChallengeTask.challenge?.id) return 0;
|
||||
const challengeId = this.brokenChallengeTask.challenge.id;
|
||||
const tasksData = this.$store.state.tasks.data;
|
||||
let count = 0;
|
||||
['habits', 'dailys', 'todos', 'rewards'].forEach(type => {
|
||||
if (tasksData[type]) {
|
||||
count += tasksData[type].filter(
|
||||
t => t.challenge && t.challenge.id === challengeId,
|
||||
).length;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
},
|
||||
modalTitle () {
|
||||
if (this.isSingleTask) {
|
||||
return this.$t('brokenTask');
|
||||
}
|
||||
if (this.brokenType === 'CHALLENGE_CLOSED') {
|
||||
return this.$t('challengeCompleted');
|
||||
}
|
||||
return this.$t('brokenChallenge');
|
||||
},
|
||||
modalSubtitle () {
|
||||
if (this.isSingleTask) {
|
||||
return this.$t('brokenTaskDescription');
|
||||
}
|
||||
if (this.brokenType === 'CHALLENGE_CLOSED') {
|
||||
return this.$t('challengeCompletedDescription', { user: this.brokenChallengeTask.challenge?.winner });
|
||||
}
|
||||
return this.$t('brokenChallengeDescription');
|
||||
},
|
||||
keepButtonText () {
|
||||
if (this.isSingleTask) {
|
||||
return this.$t('keepIt');
|
||||
}
|
||||
return this.$t('keepTasks');
|
||||
},
|
||||
removeButtonText () {
|
||||
if (this.isSingleTask) {
|
||||
return this.$t('removeIt');
|
||||
}
|
||||
return this.$t('removeTasks');
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('handle-broken-task', task => {
|
||||
this.brokenChallengeTask = { ...task };
|
||||
@@ -99,8 +231,36 @@ export default {
|
||||
unlinkOneTask: 'tasks:unlinkOneTask',
|
||||
unlinkAllTasks: 'tasks:unlinkAllTasks',
|
||||
}),
|
||||
keepAction () {
|
||||
if (this.isSingleTask) {
|
||||
this.unlink('keep');
|
||||
} else {
|
||||
this.unlink('keep-all');
|
||||
}
|
||||
},
|
||||
async removeAction () {
|
||||
if (this.isSingleTask) {
|
||||
await this.removeTask();
|
||||
} else {
|
||||
await this.unlink('remove-all');
|
||||
}
|
||||
},
|
||||
async unlink (keepOption) {
|
||||
if (keepOption.indexOf('-all') !== -1) {
|
||||
if (keepOption === 'remove-all') {
|
||||
const count = this.brokenChallengeTaskCount;
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:delete-task-confirm', {
|
||||
title: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
|
||||
description: this.$t('brokenChallengeTaskCount', { count }),
|
||||
message: this.$t('confirmDeleteTasks'),
|
||||
buttonText: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
await this.unlinkAllTasks({
|
||||
challengeId: this.brokenChallengeTask.challenge.id,
|
||||
keep: keepOption,
|
||||
@@ -122,8 +282,14 @@ export default {
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
removeTask () {
|
||||
if (!window.confirm('Are you sure you want to delete this task?')) return; // eslint-disable-line no-alert
|
||||
async removeTask () {
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:delete-task-confirm', {
|
||||
message: this.$t('sureDelete'),
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
this.destroyTask(this.brokenChallengeTask);
|
||||
this.close();
|
||||
},
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
ref="tasksList"
|
||||
class="sortable-tasks"
|
||||
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
|
||||
scrollSensitivity="64"
|
||||
scroll-sensitivity="64"
|
||||
:delay-on-touch-only="true"
|
||||
:delay="100"
|
||||
@update="taskSorted"
|
||||
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="delete-task-confirm-modal"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
modal-class="delete-confirm-modal"
|
||||
centered
|
||||
>
|
||||
<div class="modal-content-wrapper">
|
||||
<div class="top-bar"></div>
|
||||
<div class="modal-body-content">
|
||||
<div
|
||||
class="icon-wrapper"
|
||||
v-html="icons.alertIcon"
|
||||
></div>
|
||||
<h2 class="modal-title">
|
||||
{{ displayTitle }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="description"
|
||||
class="modal-description"
|
||||
>
|
||||
{{ description }}
|
||||
</p>
|
||||
<p class="modal-subtitle">
|
||||
{{ confirmationMessage }}
|
||||
</p>
|
||||
<div class="button-wrapper">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="confirm()"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
<button
|
||||
class="btn-cancel"
|
||||
@click="cancel()"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
confirmationMessage: '',
|
||||
taskType: '',
|
||||
description: '',
|
||||
customTitle: '',
|
||||
customButtonText: '',
|
||||
resolveCallback: null,
|
||||
icons: Object.freeze({
|
||||
alertIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
displayTitle () {
|
||||
if (this.customTitle) return this.customTitle;
|
||||
return this.$t('deleteType', { type: this.taskType });
|
||||
},
|
||||
buttonText () {
|
||||
if (this.customButtonText) return this.customButtonText;
|
||||
return this.displayTitle;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:delete-task-confirm', config => {
|
||||
this.confirmationMessage = config.message;
|
||||
this.taskType = config.taskType || '';
|
||||
this.description = config.description || '';
|
||||
this.customTitle = config.title || '';
|
||||
this.customButtonText = config.buttonText || '';
|
||||
this.resolveCallback = config.resolve;
|
||||
this.$root.$emit('bv::show::modal', 'delete-task-confirm-modal');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:delete-task-confirm');
|
||||
},
|
||||
methods: {
|
||||
confirm () {
|
||||
if (this.resolveCallback) {
|
||||
this.resolveCallback(true);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
cancel () {
|
||||
if (this.resolveCallback) {
|
||||
this.resolveCallback(false);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'delete-task-confirm-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
::v-deep .delete-confirm-modal {
|
||||
.modal-dialog {
|
||||
max-width: 330px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
height: 8px;
|
||||
background-color: $maroon-100;
|
||||
}
|
||||
|
||||
.modal-body-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
margin-top: 40px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
::v-deep svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
path {
|
||||
fill: #DE3F3F;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
color: $maroon-100;
|
||||
font-family: 'Roboto Condensed', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.modal-description + .modal-subtitle {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 0;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
text-align: center;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $purple-300;
|
||||
font-family: Roboto, sans-serif;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0;
|
||||
cursor: pointer;
|
||||
padding: 8px 16px;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1177,9 +1177,16 @@ export default {
|
||||
moveToBottom () {
|
||||
this.$emit('moveTo', this.task, 'bottom');
|
||||
},
|
||||
destroy () {
|
||||
async destroy () {
|
||||
const type = this.$t(this.task.type);
|
||||
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:delete-task-confirm', {
|
||||
message: this.$t('sureDeleteType', { type }),
|
||||
taskType: type,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
this.destroyTask(this.task);
|
||||
this.$emit('taskDestroyed', this.task);
|
||||
},
|
||||
|
||||
@@ -150,14 +150,14 @@
|
||||
<button
|
||||
type="button"
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.up ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleUpDirection()"
|
||||
>
|
||||
<div
|
||||
class="habit-option-button no-transition
|
||||
d-flex justify-content-center align-items-center mb-2"
|
||||
d-flex justify-content-center align-items-center mb-2"
|
||||
:class="task.up ? cssClass('bg') : ''"
|
||||
>
|
||||
<div
|
||||
@@ -176,14 +176,14 @@
|
||||
<button
|
||||
type="button"
|
||||
class="habit-option-container no-transition
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
d-flex flex-column justify-content-center align-items-center"
|
||||
:class="!task.down ? cssClass('habit-control-disabled') : ''"
|
||||
:disabled="challengeAccessRequired"
|
||||
@click="toggleDownDirection()"
|
||||
>
|
||||
<div
|
||||
class="habit-option-button no-transition
|
||||
d-flex justify-content-center align-items-center mb-2"
|
||||
d-flex justify-content-center align-items-center mb-2"
|
||||
:class="task.down ? cssClass('bg') : ''"
|
||||
>
|
||||
<div
|
||||
@@ -382,6 +382,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showStatAssignment"
|
||||
class="stat-assignment option mt-3"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label
|
||||
v-once
|
||||
class="col-12 mb-1"
|
||||
>{{ $t('assignedStat') }}</label>
|
||||
<div class="col-12">
|
||||
<div class="stat-dropdown-container">
|
||||
<select-list
|
||||
:items="statOptions"
|
||||
:value="task.attribute"
|
||||
key-prop="key"
|
||||
active-key-prop="key"
|
||||
@select="task.attribute = $event.key"
|
||||
>
|
||||
<template #item="{ item, button }">
|
||||
<div class="stat-option-content">
|
||||
<span
|
||||
class="stat-option-title"
|
||||
:class="item.key"
|
||||
>
|
||||
{{ $t(item.label) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!button"
|
||||
class="stat-option-description"
|
||||
>
|
||||
{{ $t(item.description) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</select-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'habit' && !groupId"
|
||||
class="option mt-3"
|
||||
@@ -591,7 +630,7 @@
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary btn-footer
|
||||
d-flex align-items-center justify-content-center"
|
||||
d-flex align-items-center justify-content-center"
|
||||
:class="{'btn-disabled': !canSave}"
|
||||
type="button"
|
||||
@click="submit()"
|
||||
@@ -911,6 +950,87 @@
|
||||
.streak-addon path {
|
||||
fill: $gray-200;
|
||||
}
|
||||
|
||||
.stat-dropdown-container {
|
||||
.select-list {
|
||||
.selectListItem {
|
||||
margin-bottom: 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.selectListItem .dropdown-item {
|
||||
padding: 8px 16px !important;
|
||||
height: auto !important;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: rgba($purple-600, 0.25) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stat-option-title {
|
||||
font-weight: normal;
|
||||
color: $gray-50;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-option-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
.stat-option-title {
|
||||
display: block;
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
text-transform: capitalize;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&.str {
|
||||
color: $maroon-100;
|
||||
}
|
||||
|
||||
&.int {
|
||||
color: $blue-50;
|
||||
}
|
||||
|
||||
&.con {
|
||||
color: $yellow-5;
|
||||
}
|
||||
|
||||
&.per {
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-option-description {
|
||||
display: block;
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-100;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -1023,7 +1143,6 @@
|
||||
.input-group-outer.disabled .input-group-text {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -1038,6 +1157,7 @@ import SelectMulti from './modal-controls/selectMulti';
|
||||
import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty';
|
||||
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
|
||||
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import selectList from '@/components/ui/selectList';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
|
||||
@@ -1061,6 +1181,7 @@ export default {
|
||||
selectTranslatedArray,
|
||||
toggleCheckbox,
|
||||
lockableLabel,
|
||||
selectList,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
@@ -1094,6 +1215,12 @@ export default {
|
||||
con: 'constitution',
|
||||
per: 'perception',
|
||||
},
|
||||
statOptions: [
|
||||
{ key: 'str', label: 'strength', description: 'strTaskText' },
|
||||
{ key: 'int', label: 'intelligence', description: 'intTaskText' },
|
||||
{ key: 'con', label: 'constitution', description: 'conTaskText' },
|
||||
{ key: 'per', label: 'perception', description: 'perTaskText' },
|
||||
],
|
||||
calendarHighlights: { dates: [new Date()] },
|
||||
};
|
||||
},
|
||||
@@ -1187,6 +1314,12 @@ export default {
|
||||
selectedTags () {
|
||||
return this.getTagsFor(this.task);
|
||||
},
|
||||
showStatAssignment () {
|
||||
return this.task.type !== 'reward'
|
||||
&& !this.groupId
|
||||
&& this.user.preferences.automaticAllocation === true
|
||||
&& this.user.preferences.allocationMode === 'taskbased';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
task () {
|
||||
@@ -1305,9 +1438,16 @@ export default {
|
||||
}
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
},
|
||||
destroy () {
|
||||
async destroy () {
|
||||
const type = this.$t(this.task.type);
|
||||
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:delete-task-confirm', {
|
||||
message: this.$t('sureDeleteType', { type }),
|
||||
taskType: type,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) return;
|
||||
this.destroyTask(this.task);
|
||||
this.$emit('taskDestroyed', this.task);
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<template #button-content>
|
||||
<slot
|
||||
name="item"
|
||||
:item="selected || placeholder"
|
||||
:item="selectedItem || placeholder"
|
||||
:button="true"
|
||||
>
|
||||
<!-- Fallback content -->
|
||||
@@ -134,6 +134,14 @@ export default {
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedItem () {
|
||||
if (this.activeKeyProp) {
|
||||
return this.items.find(item => item[this.activeKeyProp] === this.selected);
|
||||
}
|
||||
return this.items.find(item => item === this.selected);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getKeyProp (item) {
|
||||
return this.keyProp ? item[this.keyProp] : item.key || item.identifier;
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
>
|
||||
<label
|
||||
class="toggle-switch-label"
|
||||
|
||||
@@ -1340,7 +1340,7 @@ export default {
|
||||
},
|
||||
|
||||
openAdminPanel () {
|
||||
this.$router.push(`/admin-panel/${this.hero._id}`);
|
||||
this.$router.push(`/admin/panel/${this.hero._id}`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,15 @@ export default {
|
||||
};
|
||||
|
||||
const purchaseForKey = currencyToPurchaseForKey[currency];
|
||||
return window.confirm(this.$t(purchaseForKey, { cost })); // eslint-disable-line no-alert
|
||||
|
||||
return new Promise(resolve => {
|
||||
this.$root.$emit('habitica:purchase-confirm', {
|
||||
message: this.$t(purchaseForKey, { cost }),
|
||||
currency,
|
||||
cost,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -679,7 +679,7 @@ import NotificationMixins from '@/mixins/notifications';
|
||||
|
||||
// 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',
|
||||
|
||||
@@ -217,8 +217,18 @@ export default {
|
||||
}
|
||||
},
|
||||
async changeClassAndClose () {
|
||||
if (!this.classDisabled && !window.confirm(this.$t('changeClassConfirmCost'))) {
|
||||
return;
|
||||
if (!this.classDisabled) {
|
||||
const confirmed = await new Promise(resolve => {
|
||||
this.$root.$emit('habitica:purchase-confirm', {
|
||||
message: this.$t('changeClassConfirmCost'),
|
||||
currency: 'gems',
|
||||
cost: 3,
|
||||
resolve,
|
||||
});
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.$root.$once('bv::hide::modal', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td colspan="3"
|
||||
<td
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
colspan="3"
|
||||
>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3
|
||||
@@ -18,8 +19,9 @@
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td colspan="3"
|
||||
<td
|
||||
v-if="mixinData.inlineSettingMixin.modalVisible"
|
||||
colspan="3"
|
||||
>
|
||||
<h3
|
||||
v-once
|
||||
@@ -59,8 +61,8 @@
|
||||
{{ $t('performanceAnalytics') }}
|
||||
</label>
|
||||
<toggle-switch
|
||||
class="mb-auto"
|
||||
v-model="user.preferences.analyticsConsent"
|
||||
class="mb-auto"
|
||||
@change="prefToggled()"
|
||||
/>
|
||||
</div>
|
||||
@@ -151,14 +153,14 @@ import { mapState } from '@/libs/store';
|
||||
import alert from '@/assets/svg/for-css/alert.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
GenericUserPreferencesMixin,
|
||||
InlineSettingMixin,
|
||||
],
|
||||
components: {
|
||||
SaveCancelButtons,
|
||||
ToggleSwitch,
|
||||
},
|
||||
mixins: [
|
||||
GenericUserPreferencesMixin,
|
||||
InlineSettingMixin,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<bug-report-success-modal v-if="isUserLoaded" />
|
||||
<external-link-modal />
|
||||
<birthday-modal />
|
||||
<purchase-confirm-modal v-if="isUserLoaded" />
|
||||
<delete-task-confirm-modal v-if="isUserLoaded" />
|
||||
<template v-if="isUserLoaded">
|
||||
<privacy-banner />
|
||||
<chat-banner />
|
||||
@@ -138,6 +140,8 @@ import paymentsSuccessModal from '@/components/payments/successModal';
|
||||
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
|
||||
import subCanceledModal from '@/components/payments/canceledModal';
|
||||
import externalLinkModal from '@/components/externalLinkModal.vue';
|
||||
import purchaseConfirmModal from '@/components/shops/purchaseConfirmModal.vue';
|
||||
import deleteTaskConfirmModal from '@/components/tasks/deleteTaskConfirmModal.vue';
|
||||
|
||||
import spellsMixin from '@/mixins/spells';
|
||||
import {
|
||||
@@ -172,6 +176,8 @@ export default {
|
||||
bugReportModal,
|
||||
bugReportSuccessModal,
|
||||
externalLinkModal,
|
||||
purchaseConfirmModal,
|
||||
deleteTaskConfirmModal,
|
||||
},
|
||||
mixins: [notifications, spellsMixin],
|
||||
data () {
|
||||
@@ -262,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';
|
||||
@@ -270,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
|
||||
@@ -374,6 +359,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>
|
||||
|
||||
@@ -11,7 +11,6 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
|
||||
|
||||
// NOTE: when adding a page make sure to implement the `common:setTitle` action
|
||||
|
||||
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
|
||||
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
|
||||
|
||||
// Hall
|
||||
@@ -79,11 +78,6 @@ const router = new VueRouter({
|
||||
// in the route component to set a specific subtitle for the page.
|
||||
routes: [
|
||||
{ name: 'logout', path: '/logout', component: Logout },
|
||||
{
|
||||
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
}, {
|
||||
name: 'forgotPassword', path: '/forgot-password', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{ name: 'tasks', path: '/', component: UserTasks },
|
||||
{
|
||||
name: 'userProfile',
|
||||
|
||||
@@ -57,6 +57,9 @@ export const STATIC_ROUTES = {
|
||||
{
|
||||
name: 'features', path: 'features', component: FeaturesPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'forgotPassword', path: '/forgot-password', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
|
||||
},
|
||||
@@ -90,6 +93,9 @@ export const STATIC_ROUTES = {
|
||||
{
|
||||
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'terms', path: 'terms', component: TermsPage, meta: { requiresLogin: false },
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"achievement": "Достижения",
|
||||
"achievement": "Постижение",
|
||||
"onwards": "Напред!",
|
||||
"levelup": "Изпълнявайки целите си в истинския живот, Вие се качихте ниво и здравето Ви беше запълнено!",
|
||||
"reachedLevel": "Достигнахте Ниво <%= level %>",
|
||||
@@ -106,5 +106,25 @@
|
||||
"achievementSeasonalSpecialist": "Сезонен експерт",
|
||||
"achievementRedLetterDayText": "Са събрали всички Червени животни.",
|
||||
"achievementSeeingRedModalText": "Събрали сте всички Червени любимци!",
|
||||
"achievementSkeletonCrewText": "Събрали са всички Скелетни животни."
|
||||
"achievementSkeletonCrewText": "Събрали са всички Скелетни животни.",
|
||||
"achievementVioletsAreBlueText": "Събра всички сини пет-ове Памучен бонбон.",
|
||||
"achievementWildBlueYonderModalText": "Ти укроти всички маунт-ове Памучен бонбон синьо!",
|
||||
"achievementWildBlueYonderText": "Укроти всички маунт-ове Памучен бонбон синьо.",
|
||||
"achievementVioletsAreBlue": "Розите са червени, Теменужките са сини",
|
||||
"achievementVioletsAreBlueModalText": "Ти събра (или колекционира) всички домашни любимци (или пет-ове) от серията Памучен бонбон синьо!",
|
||||
"achievementWildBlueYonder": "Дивото синьо небе",
|
||||
"achievementSeasonalSpecialistText": "Завърши всички сезонни куестове от пролетта и зимата: Лов на яйца (Egg Hunt), Дядо Коледа-ловец (Trapper Santa) и Намери мечето (Find the Cub)!",
|
||||
"achievementSeasonalSpecialistModalText": "Вие завършихте всичките сезонни куестове!",
|
||||
"achievementDomesticatedModalText": "Ти събра (или колекционира) всички опитомени домашни любимци (пет-ове)!",
|
||||
"achievementDomesticatedText": "Излюпи (или Отгледа) всички стандартни цветове на опитомени домашни любимци (пет-ове): пор, морско свинче, петел, летящо прасе), плъх, заек, кон и крава!",
|
||||
"achievementShadyCustomerText": "Събра всички пет-ове Сянка.",
|
||||
"achievementShadyCustomerModalText": "Събра всички пет-ове Сянка.",
|
||||
"achievementShadyCustomer": "Сенчест тип",
|
||||
"achievementDomesticated": "И-Я–И–Я–ЙО",
|
||||
"achievementZodiacZookeeper": "Пазител на Зодиака",
|
||||
"achievementShadeOfItAll": "В сянката на света",
|
||||
"achievementShadeOfItAllText": "Опитоми всички сенчести коне.",
|
||||
"achievementZodiacZookeeperText": "Излюпи всички стандартни животни(базов цвят) от зодиака: Плъх, Крава, Заек, Змия, Овца, Маймуна, Кокошка, Вълк, Тигър, Летящо прасе, и Дракон!",
|
||||
"achievementZodiacZookeeperModalText": "Събрахте всички животни от зодиака!",
|
||||
"achievementBirdsOfAFeather": "От една порода"
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
"challenge": "Предизвикателство",
|
||||
"challengeDetails": "Предизвикателствата са обществени събития, в които играчите се състезават и печелят награди като изпълняват няколко свързани по някакъв начин задачи.",
|
||||
"brokenChaLink": "Повредена връзка на предизвикателство",
|
||||
"brokenTask": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но е била премахната от него. Какво бихте искали да направите?",
|
||||
"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,13 +82,12 @@
|
||||
"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": "Снаряжение за класа",
|
||||
"classGearText": "Поздравления за избора на клас! Добавих новата Ви основна екипировка в инвентара Ви. Погледнете по-долу, за да я екипирате!",
|
||||
"autoAllocate": "Автоматично разпределяне",
|
||||
"spells": "Умения",
|
||||
"skillsTitle": "Умения",
|
||||
"toDo": "Задача",
|
||||
|
||||
@@ -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": "Ключ от зверилника за превози",
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
"challenge": "Výzva",
|
||||
"challengeDetails": "Výzvy jsou komunitní události, ve kterých hráči soutěží a získávají odměny za plnění úkolů.",
|
||||
"brokenChaLink": "Nefunkční odkaz na výzvu",
|
||||
"brokenTask": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš dělat?",
|
||||
"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",
|
||||
|
||||
@@ -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,13 +82,12 @@
|
||||
"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í",
|
||||
"classGearText": "Gratuluji k vybrání povolání! Přidal jsem ti základní zbraň do tvého inventáře. Podívej se dolů a vybav se!",
|
||||
"autoAllocate": "Připisovat automaticky",
|
||||
"spells": "Dovednosti",
|
||||
"skillsTitle": "<%= classStr %> Dovednosti",
|
||||
"toDo": "úkol",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -121,7 +121,6 @@
|
||||
"yesterDailiesCallToAction": "Začít můj nový den!",
|
||||
"sessionOutdated": "Tvá relace je zastaralá. Prosím, zkus ji obnovit nebo synchronizovat.",
|
||||
"errorTemporaryItem": "Tento předmět je dočasný a nemůže být připnut.",
|
||||
"sureDeleteType": "Chceš tento <%= type %> opravdu smazat?",
|
||||
"deleteTaskType": "Tento <%= type %> smazat",
|
||||
"addNotes": "Přidej poznámky",
|
||||
"addATitle": "Přidej název",
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
"challenge": "Udfordring",
|
||||
"challengeDetails": "Udfordringer er fællesskabsevents, i hvilke spillere konkurrerer og vinder belønninger ved at fuldføre en gruppe relaterede opgaver.",
|
||||
"brokenChaLink": "Defekt udfordringslink",
|
||||
"brokenTask": "Defekt udfordringslink: denne opgave var en del af en udfordring, men er blevet fjernet fra den. Hvad vil du gøre?",
|
||||
"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",
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"allocatePerPop": "Tilføj et point til Opfattelse",
|
||||
"allocateInt": "Point tilføjet til Intelligens:",
|
||||
"allocateIntPop": "Tilføj et point til Intelligens",
|
||||
"noMoreAllocate": "Nu hvor du har nået niveau 100, vil du ikke optjene flere Egenskabspoint. Du kan fortsætte, eller starte et nyt eventyr fra niveau 1 ved at bruge <a href='http://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Genfødselskuglen</a>!",
|
||||
"noMoreAllocate": "Nu hvor du har nået niveau 100, vil du ikke optjene flere Egenskabspoint. Du kan fortsætte, eller starte et nyt eventyr fra niveau 1 ved at bruge<a href='/shops/market'>Genfødselskugle</a>.",
|
||||
"stats": "Egenskaber",
|
||||
"strength": "Styrke",
|
||||
"strText": "Styrke forøger chancen for tilfældige \"fuldtræffere\" og giver et boost til Guld, Erfaring, og chancen for at finde genstande fra dem. Det hjælper også med at påføre bossmonstre skade.",
|
||||
@@ -183,5 +183,10 @@
|
||||
"chatCastSpellParty": "<%= username %> kaster <%= spell %> for holdet.",
|
||||
"notEnoughGold": "Ikke nok guld.",
|
||||
"purchasePetItemConfirm": "Dette køb vil overskride det antal genstande du skal bruge for at klække alle mulige <%= itemText %> Kæledyr. Er du sikker?",
|
||||
"purchaseForGold": "Køb for <%= cost %> Guld?"
|
||||
"purchaseForGold": "Køb for <%= cost %> Guld?",
|
||||
"skins": "Hudfarve",
|
||||
"titleFacialHair": "Ansigts hår",
|
||||
"titleHaircolor": "Hårfarve",
|
||||
"titleHairbase": "Hår stil",
|
||||
"strTaskText": "Øg chancen for fuldtræffer slag og skade ved at færdiggøre opgaver. Dette øger også skade mod missions monstrene."
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
"questEggVelociraptorText": "Velociraptor",
|
||||
"questEggVelociraptorMountText": "Velociraptor",
|
||||
"questEggVelociraptorAdjective": "en vaks",
|
||||
"eggNotes": "Find en udrugningseliksir til at hælde på dit æg, og det vil udklække <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
|
||||
"eggNotes": "Find en udrugningseliksir til at hælde på dit æg, og det vil udklække <%= eggAdjective %> <%= eggText %>.",
|
||||
"hatchingPotionBase": "Almindelig",
|
||||
"hatchingPotionWhite": "Hvid",
|
||||
"hatchingPotionDesert": "Ørken",
|
||||
@@ -211,7 +211,7 @@
|
||||
"hatchingPotionGlow": "Selvlysende",
|
||||
"hatchingPotionFrost": "Frost",
|
||||
"hatchingPotionIcySnow": "Isnende sne",
|
||||
"hatchingPotionNotes": "Hæld denne over et æg, og det vil udklækkes til et <%= potText(locale) %> kæledyr.",
|
||||
"hatchingPotionNotes": "Hæld denne over et æg, og det vil udklækkes til et <%= potText %> kæledyr.",
|
||||
"foodMeat": "Kød",
|
||||
"foodMeatThe": "Kødet",
|
||||
"foodMeatA": "Kød",
|
||||
|
||||
@@ -13,5 +13,16 @@
|
||||
"faqQuestion28": "Kan jeg sætte mine Daglige på pause, hvis jeg har brug for en pause?",
|
||||
"webFaqAnswer28": "Ja! Knappen \"Pause Damage\" kan findes i Indstillinger. Det vil forhindre dig i at miste HP for mistede Daglige. Dette er nyttigt, hvis du er på ferie, har brug for et hvil, eller du af en eller anden grund måske har brug for en pause. Hvis du deltager i en Quest, vil dine egne afventende fremskridt blive sat på pause, men du vil stadig tage skade fra dit partimedlems missede Daglige.\n\nFor at sætte specifikke Daglige på pause, kan du redigere tidsplanen, så den forfalder hver 0. dag, indtil du er klar til at genstarte den.",
|
||||
"faqQuestion29": "Hvordan gendanner jeg HP?",
|
||||
"webFaqAnswer29": "Du kan genvinde 15 HP ved at købe en Health Potion fra din belønningskolonne for 25 guld. Derudover vil du altid genvinde fuld HP, når du stiger i niveau!"
|
||||
"webFaqAnswer29": "Du kan genvinde 15 HP ved at købe en Health Potion fra din belønningskolonne for 25 guld. Derudover vil du altid genvinde fuld HP, når du stiger i niveau!",
|
||||
"webFaqAnswer34": "Dyr kan lide mad som matcher deres farve, med undtagelse af basis dyr som alle kan lide det samme mad, uanset farve. Du kan se hvilken specifik mad de forskellige dyr kan lide, herunder: \n\n * Basis dyr kan lide kød\n * Hvide dyr kan lide mælk\n * Ørken dyr kan lide kartofler\n * Røde dyr kan lide jordbær\n * Skygge dyr kan lide chokolade\n * Skelet dyr kan lide fisk\n * Zombie dyr kan lide kød\n * Lyserøde candyfloss dyr kan lide lyserød candyfloss\n * Blå candyfloss dyr kan lide blå candyfloss\n * Gyldne dyr kan lide honning",
|
||||
"faqQuestion30": "Hvad sker der når jeg løber tør for SP?",
|
||||
"webFaqAnswer30": "Hvis din SP når ned på nul, mister du et level - det levels egenskabspoint, alt dit guld, og et stykke udstyr som kan købes igen. Du kan genvinde SP ved at færdiggøre opgaver, og ved at gå op i level igen.",
|
||||
"faqQuestion31": "Hvorfor mistede jeg SP ved at gennemføre en ikke-negativ opgave?",
|
||||
"webFaqAnswer31": "Hvis du gennemfører en opgave og mister SP du ikke burde have mistet, er der opstået en forsinkelse imens serveren synkroniserede ændringer mellem dine enheder. Fx. Hvis du bruger Guld, Magisk energi, eller mister SP på din telefon og efterfølgende gennemfører en opgave på hjemmesiden, mangler serveren bare at synkronisere dine enheder.",
|
||||
"faqQuestion32": "Hvordan vælger jeg klassering?",
|
||||
"webFaqAnswer32": "Alle spillere starter i kriger-klassen indtil de når level 10. Når du når level 10 får du valget mellem at skifte klassering eller fortsætte som kriger.\n\nHver klasse har forskelligt udstyr og færdigheder. Hvis du ikke har lyst til at skifte klassering, kan du trykke på “fravælg”. Hvis du vælger ikke at vælge klassering, kan du altid aktivere klasseringssystemet i indstillingerne senere.\n\nHvis du gerne vil skifte klasse efter level 10, kan du gøre dette ved at bruge en genfødselskugle. Genfødselskuglen vil være mulig at købe på markedet for 6 ædelsten når du når level 50, eller få den gratis ved at nå level 100.\n\nAlternativt, kan du på ethvert tidspunkt ændre klassering i indstillinger for 3 ædelsen. Dette nulstiller ikke dit level, som ellers sker ved brug af genfødselskuglen. Det vil derimod give dig mulighed for at omrokere de egenskabspoint du har samlet sammen ved at gå op i level, til at matche din nye klasse.",
|
||||
"faqQuestion33": "Hvad er den blå bjælke som kommer efter level 10?",
|
||||
"webFaqAnswer33": "Efter du låser op for klasseringssystemet kan du også låse op for færdigheder, som kræver at Magisk energi benyttes. Magisk energi bestemmes ud fra din intelligensstatus, og kan justeres med færdigheder og visse udstyr.",
|
||||
"faqQuestion34": "Hvilken slags mad kan mit dyr lide?",
|
||||
"faqQuestion35": "Mit dyr forsvandt da jeg fodrede det! Hvad skete der?"
|
||||
}
|
||||
|
||||
@@ -24,34 +24,34 @@
|
||||
"invalidEmail": "Det kræver en valid emailadresse for at få nulstillet dit kodeord.",
|
||||
"login": "Log ind",
|
||||
"logout": "Log Ud",
|
||||
"marketing1Header": "Forbedr dine vaner ved at spille et spil",
|
||||
"marketing1Lead1Title": "Dit liv, rollespillet",
|
||||
"marketing1Lead1": "Habitica er et computerspil, der hjælper med at forbedre dine vaner i virkeligheden. Det gør dit liv til et spil ved at lave alle dine opgaver (Vaner, Daglige og To-Do's) indtil små monstre, du skal besejre. Jo bedre du er til dette, desto større fremskridt vil du gøre i spillet. Hvis du begår fejl i livet, vil din karakter miste fremskridt i spillet.",
|
||||
"marketing1Lead2Title": "Få Lækkert Udstyr",
|
||||
"marketing1Lead2": "Forbedr dine vaner for at ændre din avatar. Vis det fede udstyr du har tjent!",
|
||||
"marketing1Lead3Title": "Find Tilfældige Præmier",
|
||||
"marketing1Header": "Forbedr dine vaner med spil, et level ad gangen!",
|
||||
"marketing1Lead1Title": "Gør dit liv til et spil",
|
||||
"marketing1Lead1": "Habitica er den perfekte app for alle der har svært ved at færdiggøre sin to-do liste. Vi bruger genkendelige spillemekanismer, såsom at belønne dig med Guld, Erfarring og genstande, for at hjælpe dig med at føle mere produktiv og samtidig øge følelsen af præstation, når du færdiggøre en opgave. Jo bedre du er til at færdiggøre dine opgaver, jo længere kommer du i spillet.",
|
||||
"marketing1Lead2Title": "Udstyr dig med stil",
|
||||
"marketing1Lead2": "Saml svær, udrustning og meget andet med Guld du tjener ved at gennemføre opgaver. Med mulighed for at indsamle, og vælge, mellem hundredevis af forskellige genstande, løber du aldrig tør for sammensætninger. Optimer for niveau, stil, eller begge! ",
|
||||
"marketing1Lead3Title": "Bliv belønnet for din indsats",
|
||||
"marketing1Lead3": "For nogle er det chancen, der motiverer dem: et system der hedder \"stokastiske belønninger.\" Habitica har plads til alle slags måder at forbedre eller straffe sig selv på: positive, negative, forudsigelige og tilfældige.",
|
||||
"marketing2Header": "Kæmp med venner, deltag i interessegrupper",
|
||||
"marketing2Header": "Slå dig sammen med dine venner",
|
||||
"marketing2Lead1Title": "Social produktivitet",
|
||||
"marketing2Lead1": "Selvom du selvfølgelig kan spille Habitica selv, bliver det først virkelig godt når I begynder at samarbejde, konkurrere og holde hinanden ansvarlige. Den mest effektive del af ethvert selvforbedringsprogram er social ansvarlighed, og hvad er et bedre miljø for ansvarlighed og konkurrence end et computerspil?",
|
||||
"marketing2Lead2Title": "Bekæmp monstre",
|
||||
"marketing2Lead2Title": "Bekæmp monstre i opgaver",
|
||||
"marketing2Lead2": "Hvad er et rollespil uden slåskampe? Bekæmp monstre med dit hold. Monstre er \"super-ansvarligheds-mode\" - en dag, du ikke træner, er en dag, hvor monstret skader *alle!*",
|
||||
"marketing2Lead3Title": "Udfordr Hinanden",
|
||||
"marketing2Lead3Title": "Udfordr hinanden",
|
||||
"marketing2Lead3": "Udfordringer lader dig konkurrere med venner og fremmede. Den, der er bedst i slutningen af en udfordring vinder særlige præmier.",
|
||||
"marketing3Header": "Apps og Udvidelser",
|
||||
"marketing3Header": "Forskellige måder at bruge Habitica på",
|
||||
"marketing3Lead1": "**iPhone & Android** apps lader dig klare dine ting på farten. Vi ved, at det nogen gange er for meget at skulle logge ind på websiden for at klikke på knapper.",
|
||||
"marketing3Lead2Title": "Integrationer",
|
||||
"marketing3Lead2Title": "Udviklet af fællesskabet",
|
||||
"marketing3Lead2": "Andre **tredjepartsværktøjer** kan binde Habitica sammen med andre dele af dit liv. Vores API muliggør integrationer som [Chrome Extension](https://chrome.google.com/webstore/detail/habitica/pidkmpibnnnhneohdgjclfdjpijggmjj?hl=en-US), med hvilken du mister point ved at bruge unyttige hjemmesider, og optjener point når du browser de nyttige i stedet. [Se mere her](https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations).",
|
||||
"marketing4Header": "Organisatorisk brug",
|
||||
"marketing4Header": "Udover hverdagens pligter",
|
||||
"marketing4Lead1": "Uddannelse er en af de bedste områder at bruge spilelementer. Vi ved alle, hvordan studerende nærmest er limet til deres telefon disse dage, så brug dette! Sæt dine elever til at kæmpe mod hinanden som hyggelig konkurrence. Beløn god opførsel med sjældne præmier. Se deres karakterer og opførsel blive forbedret.",
|
||||
"marketing4Lead1Title": "Brug af Spilelementer i Undervisning",
|
||||
"marketing4Lead1Title": "Spilelementer i undervisning",
|
||||
"marketing4Lead2": "Prisen for sundhedssektoren stiger, og der må ske noget. Hundredevis af programmer er bygget til at reducere udgifter og forbedre velvære. Vi tror på, at Habitica kan bane vejen for en sundere livsstil.",
|
||||
"marketing4Lead2Title": "Brug af Spilelementer i Sundhed og Velvære",
|
||||
"marketing4Lead3-1": "Vil du gøre dit liv til et spil?",
|
||||
"marketing4Lead2Title": "Spilelementer i sundhed og velvære",
|
||||
"marketing4Lead3-1": "Klar til at udrette noget imens du har det sjovt?",
|
||||
"marketing4Lead3-2": "Interesseret i at lede en gruppe for uddannelse, velvære og mere?",
|
||||
"marketing4Lead3Title": "Spilificér Alt",
|
||||
"mobileAndroid": "Android",
|
||||
"mobileIOS": "iOS",
|
||||
"marketing4Lead3Title": "Start din rejse!",
|
||||
"mobileAndroid": "Android App",
|
||||
"mobileIOS": "iOS App",
|
||||
"oldNews": "Nyheder",
|
||||
"setNewPass": "Sæt nyt kodeord",
|
||||
"password": "Kodeord",
|
||||
@@ -110,7 +110,7 @@
|
||||
"missingPassword": "Manglende kodeord.",
|
||||
"missingNewPassword": "Manglende nyt kodeord.",
|
||||
"invalidEmailDomain": "Du kan ikke registrere med emails med følgende domæner: <%= domains %>",
|
||||
"wrongPassword": "Forkert kodeord.",
|
||||
"wrongPassword": "Forkert kodeord. Hvis du har glemt dit kodeord, tryk på “Glemt kodeord”.",
|
||||
"incorrectDeletePhrase": "Skriv venligst <%= magicWord %> med store bogstaver for at slette din konto.",
|
||||
"notAnEmail": "Ugyldig e-mailadresse.",
|
||||
"emailTaken": "E-mailadressen er allerede brugt til en konto.",
|
||||
@@ -121,8 +121,8 @@
|
||||
"usernameTaken": "Brugernavn allerede taget.",
|
||||
"passwordConfirmationMatch": "Kodeord og godkendelse er ikke ens.",
|
||||
"passwordResetPage": "Nulstil kodeord",
|
||||
"passwordReset": "Hvis vi har din email på lager, vil instruktioner til opsætning af nyt kodeord være sendt til din email.",
|
||||
"invalidLoginCredentialsLong": "Åh-åh - din emailaddresse/brugernavn eller kodeord er forkert.\n- Vær sikker på du har indtastet korrekt. Der gøres forskel på store og små bogstaver.\n- Du kan have brugt Facebook eller Google til at oprette din konto, ikke email, så dobbeltcheck ved at prøve dem.\n- Hvis du har glemt dit kodeord, så klik på \"Glemt kodeord\".",
|
||||
"passwordReset": "Hvis din E-mail eller brugernavn allerede findes i vores system, vil instruktioner til opsætning af nyt kodeord være sendt til din email.",
|
||||
"invalidLoginCredentialsLong": "Åh-åh - din e-mailadresse, brugernavn eller kodeord er forkert.\nPrøv igen, eller klik på \"Glemt kodeord\".",
|
||||
"invalidCredentials": "Der er ingen konto med disse legitimationsoplysninger.",
|
||||
"accountSuspended": "Denne konto, bruger-ID \"<%= userId %>\", er blevet blokeret for at bryde Retningslinjerne for fællesskabet (https://habitica.com/static/community-guidelines) eller vores Vilkår og betingelser (https://habitica.com/static/terms). For at få detaljer eller bede om at få blokaden ophævet, så send venligst en email til vores Community Manager på <%= communityManagerEmail %>, eller bed din forælder eller værge om at gøre det. Inkluder venligst dit @Brugernavn i emailen.",
|
||||
"accountSuspendedTitle": "Kontoen er blevet suspenderet",
|
||||
@@ -132,14 +132,14 @@
|
||||
"invalidReqParams": "Ugyldige anmodningsparametre.",
|
||||
"memberIdRequired": "\"member\" skal være et gyldigt Unikt Bruger-ID.",
|
||||
"heroIdRequired": "\"heroID\" skal være et gyldigt Unikt Bruger-ID.",
|
||||
"cannotFulfillReq": "Din anmodning kan ikke udføres. Kontakt admin@habitica.com hvis fejlen fortsætter.",
|
||||
"cannotFulfillReq": "Denne e-mail er allerede taget i brug. Prøv at logge ind eller brug en anden e-mail til registrering. Hvis du har brug for hjælp, kontakt os på admin@habitica.com",
|
||||
"modelNotFound": "Denne model findes ikke.",
|
||||
"signUpWithSocial": "Tilmeld med <%= social %>",
|
||||
"signUpWithSocial": "Fortsæt med <%= social %>",
|
||||
"loginWithSocial": "Log in med<%= social %>",
|
||||
"confirmPassword": "Bekræft kodeord",
|
||||
"usernameLimitations": "Brugernavn skal være 1 til 20 tegn, kun bruge bogstaver fra a til z, tal fra 0 til 9, bindestreg eller bundstreg (_), og kan ikke inkludere nogle upassende ord.",
|
||||
"usernameLimitations": "Brugernavn kan ændres på alle tidspunkter. Det skal være 1 til 20 tegn, kun indeholde bogstaver fra a til z, tal fra 0 til 9, bindestreg eller understreg (_)",
|
||||
"usernamePlaceholder": "fx HabitRabbit",
|
||||
"emailPlaceholder": "e.g., rabbit@example.com",
|
||||
"emailPlaceholder": "Fx., kanin@eksempel.com",
|
||||
"passwordPlaceholder": "e.g., ******************",
|
||||
"confirmPasswordPlaceholder": "Sikr dig at det er det samme kodeord!",
|
||||
"joinHabitica": "Opret bruger",
|
||||
@@ -169,10 +169,22 @@
|
||||
"joinMany": "Tilslut dig over <%= userCountInMillions %> millioner andre, der har det sjovt imens de opnår deres mål!",
|
||||
"joinToday": "Tilmeld dig Habitica i dag",
|
||||
"signup": "Tilmeld dig",
|
||||
"getStarted": "Kom i gang!",
|
||||
"getStarted": "Kom i gang",
|
||||
"mobileApps": "Mobile apps",
|
||||
"learnMore": "Lær mere",
|
||||
"communityInstagram": "Instagram",
|
||||
"minPasswordLength": "Kodeord skal bestå af 8 eller flere tegn.",
|
||||
"enterHabitica": "Spil Habitica"
|
||||
"enterHabitica": "Spil Habitica",
|
||||
"footerProduct": "Produkt",
|
||||
"marketing4Lead3Button": "Start i dag",
|
||||
"incorrectResetPhrase": "Skriv <%= magicWord %> i store bogstaver for at nulstille din konto.",
|
||||
"minPasswordLengthLogin": "Dit kodeord er minimum 8 karakterer langt.",
|
||||
"enterValidEmail": "Indtast venligst en gyldig e-mailadresse.",
|
||||
"translateHabitica": "Oversæt Habitica",
|
||||
"whatToCallYou": "Hvad skal vi kalde dig?",
|
||||
"acceptPrivacyTOS": "Bekræfter du at du er over 18 år gammel, og har læst og accepteret vores <a href='/static/terms' target='_blank'>Terms of Services</a> og <a href='/static/privacy' target='_blank'>Privatlivspolitik</a>",
|
||||
"emailBlockedRegistration": "Denne e-mail er blokeret, og kan ikke benyttes til registrering. Hvis der er sket en fejl, kontakt os på admin@habitica.com",
|
||||
"emailUsernamePlaceholder": "Fx., vanedyr eller kanin@eksempel.com",
|
||||
"marketing3Lead1Title": "Android & iOS apps",
|
||||
"socialAlreadyExists": "Dette login benyttes allerede af en eksisterende Habitica konto."
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
"messages": "Beskeder",
|
||||
"emptyMessagesLine1": "Du har ingen beskeder",
|
||||
"emptyMessagesLine2": "Du kan sende en ny besked til en bruger ved at besøge deres profil og klikke på \"Besked\"-knappen.",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> sendte en besked",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%= user %></span> sendte en besked",
|
||||
"letsgo": "Lad os komme i gang!",
|
||||
"selected": "Valgt",
|
||||
"howManyToBuy": "Hvor mange vil du købe?",
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"userId": "Bruger-ID",
|
||||
"invite": "Invitér",
|
||||
"leave": "Forlad",
|
||||
"invitedToParty": "Du blev inviteret til at være med på Holdet <span class=\"notification-bold\"><%- party %></span>",
|
||||
"invitedToPrivateGuild": "Du blev inviteret til den private Klan <span class=\"notification-bold\"><%- guild %></span>",
|
||||
"invitedToPublicGuild": "Du blev inviteret til Klanen <span class=\"notification-bold-blue\"><%- guild %></span>",
|
||||
"invitedToParty": "Du blev inviteret til at være med på Holdet <span class=\"notification-bold\"><%= party %></span>",
|
||||
"invitedToPrivateGuild": "Du blev inviteret til den private Klan <span class=\"notification-bold\"><%= guild %></span>",
|
||||
"invitedToPublicGuild": "Du blev inviteret til Klanen <span class=\"notification-bold-blue\"><%= guild %></span>",
|
||||
"invitationAcceptedHeader": "Din invitation blev accepteret",
|
||||
"invitationAcceptedBody": "<%= username %> har accepteret din invitation til <%= groupName %>!",
|
||||
"systemMessage": "Systembesked",
|
||||
"newMsgGuild": "<span class=\"notification-bold-blue\"><%- name %></span> har nye indlæg",
|
||||
"newMsgParty": "Dit Hold, <span class=\"notification-bold-blue\"><%- name %></span>, har nye indlæg",
|
||||
"newMsgGuild": "<span class=\"notification-bold-blue\"><%= name %></span> har nye indlæg",
|
||||
"newMsgParty": "Dit Hold, <span class=\"notification-bold-blue\"><%= name %></span>, har nye indlæg",
|
||||
"chat": "Chat",
|
||||
"sendChat": "Send besked",
|
||||
"group": "Gruppe",
|
||||
@@ -151,14 +151,14 @@
|
||||
"onlyGroupLeaderCanEditTasks": "Du har ikke rettigheder til at administrere opgaver!",
|
||||
"onlyGroupTasksCanBeAssigned": "Kun gruppeopgaver kan blive tildelt",
|
||||
"assignedTo": "Tildelt",
|
||||
"assignedToUser": "Tildelt <%- userName %>",
|
||||
"assignedToUser": "Tildelt <%= userName %>",
|
||||
"assignedToMembers": "Tildelt <%= userCount %> medlemmer",
|
||||
"assignedToYouAndMembers": "Tildelt dig og <%= userCount %> medlemmer",
|
||||
"youAreAssigned": "Du er blevet bedt om at udføre denne opgave",
|
||||
"taskIsUnassigned": "Denne opgave er ikke tildelt nogen",
|
||||
"confirmUnClaim": "Er du sikker på, du vil give afkald på denne opgave?",
|
||||
"confirmNeedsWork": "Er du sikker på, du vil markere denne opgave som ufuldstændig?",
|
||||
"userRequestsApproval": "<%- userName %> anmoder om godkendelse",
|
||||
"userRequestsApproval": "<%= userName %> anmoder om godkendelse",
|
||||
"userCountRequestsApproval": "<%= userCount %> medlemmer anmoder om godkendelse",
|
||||
"youAreRequestingApproval": "Du har anmodet om godkendelse",
|
||||
"chatPrivilegesRevoked": "Dine chatprivilegier er blevet inddraget, så du kan ikke udføre denne handling.",
|
||||
@@ -168,9 +168,9 @@
|
||||
"claim": "Gør krav på",
|
||||
"removeClaim": "Giv afkald på",
|
||||
"onlyGroupLeaderCanManageSubscription": "Kun gruppelederen kan styre gruppens abonnement",
|
||||
"yourTaskHasBeenApproved": "Din opgave, <span class=\"notification-green\"><%- taskText %></span>, er blevet godkendt.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> markerede <span class=\"notification-bold\"><%- taskText %></span> som ufuldstændig.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> anmoder om godkendelse for <span class=\"notification-bold\"><%- taskName %></span>",
|
||||
"yourTaskHasBeenApproved": "Din opgave, <span class=\"notification-green\"><%= taskText %></span>, er blevet godkendt.",
|
||||
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> markerede <span class=\"notification-bold\"><%= taskText %></span> som ufuldstændig.",
|
||||
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> anmoder om godkendelse for <span class=\"notification-bold\"><%= taskName %></span>",
|
||||
"approve": "Godkend",
|
||||
"approveTask": "Godkend opgave",
|
||||
"needsWork": "Ufuldstændig",
|
||||
@@ -183,8 +183,8 @@
|
||||
"userIsClamingTask": "`<%= username %> har gjort krav på:` <%= task %>",
|
||||
"approvalRequested": "Godkendelse efterspurgt",
|
||||
"cantDeleteAssignedGroupTasks": "Du kan ikke slette gruppeopgaver, der er blevet tildelt dig.",
|
||||
"groupPlanUpgraded": "<strong><%- groupName %></strong> blev opgraderet til en Gruppeplan!",
|
||||
"groupPlanCreated": "<strong><%- groupName %></strong> blev oprettet!",
|
||||
"groupPlanUpgraded": "<strong><%= groupName %></strong> blev opgraderet til en Gruppeplan!",
|
||||
"groupPlanCreated": "<strong><%= groupName %></strong> blev oprettet!",
|
||||
"onlyGroupLeaderCanInviteToGroupPlan": "Kun gruppelederen kan invitere brugere til en gruppe med et abonnement.",
|
||||
"paymentDetails": "Betalingsdetaljer",
|
||||
"aboutToJoinCancelledGroupPlan": "Du er ved at slutte dig til en gruppe med en opsagt gruppeplan. Du vil IKKE få et gratis abonnement.",
|
||||
@@ -321,7 +321,7 @@
|
||||
"allAssignedCompletion": "All - Completes when all assigned users finish",
|
||||
"pmReported": "Tak, fordi du rapporterede denne besked.",
|
||||
"features": "Funktioner",
|
||||
"invitedToPartyBy": "<a href=\"/profile/<%- userId %>\" target=\"_blank\">@<%- userName %></a> har inviteret dig til holdet <span class=\"notification-bold\"><%- party %></span>",
|
||||
"invitedToPartyBy": "<a href=\"/profile/<%= userId %>\" target=\"_blank\">@<%= userName %></a> har inviteret dig til holdet <span class=\"notification-bold\"><%= party %></span>",
|
||||
"PMUserDoesNotReceiveMessages": "Denne bruger modtager ikke længere private beskeder",
|
||||
"blockedToSendToThisUser": "Du kan ikke sende til denne spiller da du har blokeret denne spiller.",
|
||||
"blockYourself": "Du kan ikke blokere dig selv",
|
||||
|
||||
@@ -82,13 +82,12 @@
|
||||
"paymentMethods": "Køb med",
|
||||
"paymentSuccessful": "Din betaling gik igennem!",
|
||||
"paymentYouReceived": "Du modtog:",
|
||||
"paymentYouSentGems": "Du sendte <strong><%- name %></strong>:",
|
||||
"paymentYouSentSubscription": "Du sendte <strong><%- name %></strong> et <%= months %>-måneders Habitica-abonnement.",
|
||||
"paymentYouSentGems": "Du sendte <strong><%= name %></strong>:",
|
||||
"paymentYouSentSubscription": "Du sendte <strong><%= name %></strong> et <%= months %>-måneders Habitica-abonnement.",
|
||||
"paymentSubBilling": "Betalingen for dit abonnement vil blive trukket <strong>$<%= amount %></strong> hver <strong><%= months %> måneder</strong>.",
|
||||
"success": "Succes!",
|
||||
"classGear": "Klasseudstyr",
|
||||
"classGearText": "Tillykke med at vælge klasse! Jeg har tilføjet dit nye basisudstyr til dit inventar. Tag et kig forneden og tag det på!",
|
||||
"autoAllocate": "Tildel automatisk",
|
||||
"spells": "Evner",
|
||||
"skillsTitle": "<%= classStr %> Evner",
|
||||
"toDo": "To Do",
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
"mountNotOwned": "Du ejer ikke dette ridedyr.",
|
||||
"feedPet": "Giv <%= text %> til din <%= name %>?",
|
||||
"raisedPet": "Du har opdrættet din/dit <%= pet %>!",
|
||||
"petName": "<%= potion(locale) %> <%= egg(locale) %>",
|
||||
"mountName": "<%= potion(locale) %>-<%= mount(locale) %>",
|
||||
"petName": "<%= potion %> <%= egg %>",
|
||||
"mountName": "<%= potion %>-<%= mount %>",
|
||||
"keyToPets": "Nøgle til Kæledyrskennelen",
|
||||
"keyToPetsDesc": "Sæt alle standardkæledyrene fri, så du kan samle dem igen. (Kæledyr fra quests og sjældne kæledyr påvirkes ikke.)",
|
||||
"keyToMounts": "Nøgle til Ridedyrskennelen",
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
"userData": "Brugerdata",
|
||||
"exportUserData": "Eksportér brugerdata:",
|
||||
"export": "Eksportér",
|
||||
"xml": "(XML)",
|
||||
"json": "(JSON)",
|
||||
"xml": "XML",
|
||||
"json": "JSON",
|
||||
"customDayStart": "Brugerdefineret starttidspunkt",
|
||||
"sureChangeCustomDayStartTime": "Er du sikker på, at du vil ændre dit brugerdefinerede starttidspunkt? Dine Daglige vil blive opdateret næste gang du bruger Habitica efter <%= time %>. Vær sikker på, at du har udført dine Daglige før da!",
|
||||
"customDayStartHasChanged": "Dit brugerdefinerede starttidspunkt er ændret.",
|
||||
@@ -46,7 +46,7 @@
|
||||
"newUsername": "Nyt brugernavn",
|
||||
"dangerZone": "Farezone",
|
||||
"resetText1": "ADVARSEL! Dette nulstiller mange dele af din konto. Vi fraråder på det kraftigste dette, men nogen finder det brugbart i begyndelsen efter at have eksperimenteret med Habitica i et kort stykke tid.",
|
||||
"resetText2": "Du vil miste alle niveauer, Guld, og Erfaringspoint. Alle dine Opgaver (bortset fra dem fra Udfordringer) vil blive slettet permanent, og du vil miste al deres historik. Du vil miste alt dit udstyr, undtagen gratis gave-udstyr eller Mystiske abonnentsgenstande. Du vil være i stand til at købe alle de mistede genstand igen, inklusiv tidsbegrænset udstyr (du skal være den korrekte klasse for at købe klasse-begrænset udstyr). Du vil beholde din nuværende klasse, præstationer og dine kæle- og ridedyr. Du ville måske foretrække at bruge en Genfødselskugle i stedet. Det er en meget sikrere mulighed, og du vil beholde dine opgaver og udstyr.",
|
||||
"resetText2": "En anden mulighed er at bruge en <b>Genfødselskugle</b>, hvilket vil nulstille alt, men du vil beholde dine opgaver og dit udstyr.",
|
||||
"deleteLocalAccountText": "Er du sikker? Dette vil slette din konto for evigt, og den kan aldrig gendannes! Du skal registrere en ny konto for at kunne bruge Habitica igen. Ædelsten du har brugt eller har på lager vil ikke blive refunderet. Hvis du er fuldstændig sikker, så skriv dit kodeord i boksen herunder.",
|
||||
"deleteSocialAccountText": "Er du sikker? Dette vil slette din brugerkonto for evigt, og den kan aldrig gendannes! Du vil være nødt til at oprette en ny konto for at bruge Habitica igen. Ædelsten i en Klanbank eller som er blevet brugt vil ikke blive refunderet. Hvis du virkelig er helt sikker, så indtast \"<%= magicWord %>\" i tekstboksen nedenunder.",
|
||||
"API": "API",
|
||||
@@ -176,5 +176,24 @@
|
||||
"account": "Konto",
|
||||
"loginMethods": "Login metoder",
|
||||
"character": "Karakter",
|
||||
"siteLanguage": "Webstedets sprog"
|
||||
"siteLanguage": "Webstedets sprog",
|
||||
"showLevelUpModal": "Når man går et level op",
|
||||
"showHatchPetModal": "Når man udruger et dyr",
|
||||
"showRaisePetModal": "Når man opgradere et dyr til ridedyr",
|
||||
"showStreakModal": "Når man modtager en streak præstation",
|
||||
"baileyAnnouncement": "Seneste Bailey meddelelse",
|
||||
"view": "Vis",
|
||||
"feedbackPlaceholder": "Tilføj til feedback",
|
||||
"downloadCSV": "Download CSV",
|
||||
"downloadAs": "Download As",
|
||||
"yourUserData": "Dine brugerdata",
|
||||
"taskHistory": "Opgave historie",
|
||||
"resetTextLocal": "Hvis du er helt sikker, indsæt dit kodeord i tekstfeltet nedenfor.",
|
||||
"yourUserDataDisclaimer": "Her kan du downloade en kopi af din opgavehistorie og dine bruger data.",
|
||||
"useridCopied": "BrugerID er kopieret til udklipsholderen.",
|
||||
"developerMode": "Udviklingsmode",
|
||||
"api": "API",
|
||||
"currentPass": "Nuværende kodeord",
|
||||
"resetDetail1": "Du vil miste alle dine levels, dit Guld, og Erfaringspoint.",
|
||||
"resetDetail2": "Du vil forblive i din valgte klasse, og beholde dine præstationer, dyr og ridedyr."
|
||||
}
|
||||
|
||||
@@ -921,5 +921,14 @@
|
||||
"backgroundInsideForestWitchsCottageNotes": "Webe Zauber in der Hütte der Waldhexe.",
|
||||
"backgroundCastleKeepWithBannersText": "Burghalle mit Bannern",
|
||||
"backgroundCastleKeepWithBannersNotes": "Singe Geschichten von Heldentaten in einer Burghalle mit Bannern.",
|
||||
"backgrounds112025": "SET 138: Veröffentlicht im November 2025"
|
||||
"backgrounds112025": "SET 138: Veröffentlicht im November 2025",
|
||||
"backgrounds122025": "SET 139: Veröffentlicht im Dezember 2025",
|
||||
"backgroundNighttimeStreetWithShopsNotes": "Genieße das warme Leuchten einer Nächtlichen Straße mit Geschäften.",
|
||||
"backgroundNighttimeStreetWithShopsText": "Nächtliche Straße mit Geschäften",
|
||||
"backgrounds012026": "SET 140: Veröffentlicht im Januar 2026",
|
||||
"backgrounds022026": "SET 141: Veröffentlicht im Februar 2026",
|
||||
"backgroundElegantPalaceText": "Eleganter Palast",
|
||||
"backgroundElegantPalaceNotes": "Bewundere die farbenfrohen Hallen eines Eleganten Palastes.",
|
||||
"backgroundWinterDesertWithSaguarosText": "Winter-Wüste mit Kakteen",
|
||||
"backgroundWinterDesertWithSaguarosNotes": "Atme die kalte Luft Wunder Winter-Wüste mit Kakteen."
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
"challenge": "Herausforderung",
|
||||
"challengeDetails": "Herausforderungen sind Gemeinschaftsereignisse, an denen Spieler teilnehmen und Preise gewinnen können, indem sie die dazugehörigen Aufgaben erledigen.",
|
||||
"brokenChaLink": "Toter Herausforderungs-Link",
|
||||
"brokenTask": "Toter Herausforderungs-Link: Diese Aufgabe war Teil einer Herausforderung, aber ist mittlerweile entfernt worden. Was möchtest Du tun?",
|
||||
"keepIt": "Behalten",
|
||||
"removeIt": "Entfernen",
|
||||
"brokenChallenge": "Toter Herausforderungs-Link: Diese Aufgabe war Teil einer Herausforderung, aber die Herausforderung (oder Gruppe) wurde gelöscht. Was möchtest Du mit den verwaisten Aufgaben tun?",
|
||||
"challengeCompleted": "Diese Herausforderung ist beendet, und gewonnen hat <span class=\"badge\"><%- user %></span>! Was soll mit den verwaisten Aufgaben geschehen?",
|
||||
"brokenChallenge": "Defekter Herausforderungslink",
|
||||
"challengeCompleted": "Herausforderung abgeschlossen!",
|
||||
"unsubChallenge": "Toter Herausforderungs-Link: Diese Aufgabe war Teil einer Herausforderung, aber Du hast aufgehört an dieser Herausforderungs teilzunehmen. Was soll mit den verwaisten Aufgaben geschehen?",
|
||||
"challenges": "Herausforderungen",
|
||||
"endDate": "Endet",
|
||||
@@ -109,5 +108,9 @@
|
||||
"cannotClone": "Diese Herausforderung kann nicht dupliziert werden, weil einer oder mehrere Spieler sie als unangemessen gemeldet haben. Einer der Mitarbeiter wird dich in Kürze mit Anweisungen kontaktieren. Wenn mehr als 48 Stunden vergangen sind, und du nichts von ihnen gehört hast, schicke bitte eine Email an admin@habitica.com, um Unterstützung zu erhalten.",
|
||||
"resetFlags": "Markierungen zurücksetzen",
|
||||
"messageChallengeFlagOfficial": "Offizielle Herausforderungen können nicht gemeldet werden.",
|
||||
"deleteChallengeRefundDescription": "Wenn du diese Herausforderung löschst, bekommst du den Preis in Edelsteinen erstattet und die Aufgaben der Herausforderung verbleiben auf der Aufgabentafel der Teilnehmer."
|
||||
"deleteChallengeRefundDescription": "Wenn du diese Herausforderung löschst, bekommst du den Preis in Edelsteinen erstattet und die Aufgaben der Herausforderung verbleiben auf der Aufgabentafel der Teilnehmer.",
|
||||
"brokenTaskDescription": "Diese Aufgabe war Teil einer Herausforderung, wurde jedoch daraus entfernt. Was möchtest Du tun?",
|
||||
"brokenChallengeDescription": "Diese Aufgabe war Teil einer Herausforderung, aber die Herausforderung (oder Gruppe) wurde gelöscht. Was soll mit den verwaisten Aufgaben geschehen?",
|
||||
"challengeCompletedDescription": "Gewonnen hat <%= user %>! Was soll mit den verwaisten Aufgaben geschehen?",
|
||||
"brokenTask": "Defekter Link zur Herausforderung"
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"allocatePerPop": "Erhöhe Wahrnehmung um einen Punkt",
|
||||
"allocateInt": "Zugewiesene Intelligenzpunkte:",
|
||||
"allocateIntPop": "Erhöhe Intelligenz um einen Punkt",
|
||||
"noMoreAllocate": "Jetzt, nach dem Erreichen von Level 100, wirst Du keine weiteren Attributpunkte erhalten. Du kannst weiter aufsteigen oder ein neues Abenteuer auf Level 1 anfangen, indem Du die <a href='/shops/market'>Sphäre der Wiedergeburt</a> benutzt!",
|
||||
"noMoreAllocate": "Nachdem du nun Level 100 erreicht hast, erhältst du keine weiteren Attributpunkte mehr. Du kannst weiter aufsteigen oder mit der <a href=‚/shops/market‘>Sphäre der Wiedergeburt</a> ein neues Abenteuer auf Level 1 beginnen.",
|
||||
"stats": "Attributwerte",
|
||||
"strength": "Stärke",
|
||||
"strText": "Stärke erhöht die Wahrscheinlichkeit zufälliger \"kritischer Treffer\" und die Rate mit der durch sie Gold, Beute und Erfahrung gewonnen wird. Und hilft auch dabei, Boss-Monstern Schaden zuzufügen.",
|
||||
@@ -114,12 +114,12 @@
|
||||
"unallocated": "Freie Attributspunkte",
|
||||
"autoAllocation": "Verteilungsmuster",
|
||||
"autoAllocationPop": "Verteilt gemäß Deiner Einstellungen Punkte auf Deine Attribute, wenn Du ein Level aufsteigst.",
|
||||
"evenAllocation": "Attributspunkte gleichmäßig verteilen",
|
||||
"evenAllocationPop": "Weist jedem Statuswert die gleiche Anzahl an Punkten zu.",
|
||||
"evenAllocation": "Gleichmäßig verteilen",
|
||||
"evenAllocationPop": "Weist jedem Attribut die gleiche Anzahl von Punkten zu.",
|
||||
"classAllocation": "Punkte anhand der Klasse verteilen",
|
||||
"classAllocationPop": "Weist den Statuswerten die wichtig für Deine Klasse sind, mehr Punkte zu.",
|
||||
"taskAllocation": "Verteile Punkte abhängig von Aufgaben-Aktivität",
|
||||
"taskAllocationPop": "Verteilt Punkte abhängig davon, welche Kategorien die Aufgaben haben, die Du überwiegend erledigst: Stärke, Intelligenz, Ausdauer oder Wahrnehmung.",
|
||||
"classAllocationPop": "Weist den Attributen die wichtig für Deine Klasse sind, mehr Punkte zu.",
|
||||
"taskAllocation": "Verteile Punkte abhängig von Aufgabenaktivität",
|
||||
"taskAllocationPop": "Verteilt Punkte basierend auf den Kategorien Stärke, Intelligenz, Konstitution und Wahrnehmung, die mit den von Dir erledigten Aufgaben verbunden sind.",
|
||||
"distributePoints": "Verteile freie Punkte automatisch",
|
||||
"distributePointsPop": "Verteilt alle freien Attributpunkte gemäß Deinem gewählten Verteilungsmuster.",
|
||||
"warriorText": "Krieger verursachen mehr und stärkere \"kritische Treffer\", die zufällige Boni auf Gold, Erfahrung und Beute beim Erfüllen einer Aufgabe geben. Sie sind auch sehr stark gegen Bossmonster. Spiele einen Krieger, wenn Dich die Chance auf Belohnungen im Lottogewinn-Stil besonders reizt und Du besonders effektiv gegen Bossmonster sein willst!",
|
||||
@@ -191,5 +191,14 @@
|
||||
"titleHaircolor": "Haarfarben",
|
||||
"titleFacialHair": "Bärte",
|
||||
"titleHairbase": "Frisuren",
|
||||
"customizations": "Individualisierungen"
|
||||
"customizations": "Individualisierungen",
|
||||
"strTaskText": "Erhöht die kritische Trefferchance und den Schaden beim Erfüllen von Aufgaben. Erhöht außerdem den Schaden, der Quest-Bossen zugefügt wird.",
|
||||
"conTaskText": "Reduziert den Schaden, der durch verpasste tägliche Aufgaben und negative Gewohnheiten verursacht wird. Reduziert nicht den Schaden durch Quest-Bosse.",
|
||||
"autoAllocate": "Automatische Zuweisung",
|
||||
"pointsAvailable": "Verfügbare Punkte",
|
||||
"allocationMethod": "Zuteilungsmethode",
|
||||
"assignedStat": "Zugewiesener Wert",
|
||||
"intTaskText": "Erhöht die durch Aufgaben gesammelte Erfahrung. Erhöht außerdem deine Manakapazität und Manaregenerationsrate.",
|
||||
"perTaskText": "Erhöht die Drop-Chance für Gegenstände, die tägliche Drop-Obergrenze für Gegenstände, die Serienboni für Aufgaben und das beim Abschließen von Aufgaben verdiente Gold.",
|
||||
"statAllocationInfo": "Mit jedem Level erhältst Du einen Punkt, den Du einem Attribut Deiner Wahl zuweisen kannst. Du kannst die Zuweisung manuell vornehmen oder es dem Spiel überlassen, indem Du eine der Optionen zur automatischen Zuweisung wählst."
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
"questEggVelociraptorText": "Velociraptor-Haustier",
|
||||
"questEggVelociraptorMountText": "Velociraptor-Reittier",
|
||||
"questEggVelociraptorAdjective": "ein cleveres",
|
||||
"eggNotes": "Finde ein Schlüpfelixier, das Du über dieses Ei gießen kannst, damit ein <%= eggAdjective(locale) %> <%= eggText(locale) %> schlüpfen kann.",
|
||||
"eggNotes": "Finde ein Schlüpfelixier, das Du über dieses Ei gießen kannst, damit ein <%= eggAdjective %> <%= eggText %> schlüpfen kann.",
|
||||
"hatchingPotionBase": "Normales",
|
||||
"hatchingPotionWhite": "Weißes",
|
||||
"hatchingPotionDesert": "Wüstenfarbenes",
|
||||
@@ -211,7 +211,7 @@
|
||||
"hatchingPotionGlow": "Fluoreszierendes",
|
||||
"hatchingPotionFrost": "Frostiges",
|
||||
"hatchingPotionIcySnow": "Eisschnee",
|
||||
"hatchingPotionNotes": "Gieße dies über ein Ei und es wird ein <%= potText(locale) %> Haustier daraus schlüpfen.",
|
||||
"hatchingPotionNotes": "Gieße dies über ein Ei und es wird ein <%= potText %> Haustier daraus schlüpfen.",
|
||||
"foodMeat": "Fleisch",
|
||||
"foodMeatThe": "das Fleisch",
|
||||
"foodMeatA": "Fleisch",
|
||||
@@ -406,7 +406,7 @@
|
||||
"hatchingPotionBalloon": "Ballon",
|
||||
"wackyPotionAddlNotes": "Kann nicht zum Reittier großgezogen oder für Quest-Haustier Eier benutzt werden.",
|
||||
"hatchingPotionCryptid": "Kryptisch",
|
||||
"wackyPotionNotes": "Schütte dies über ein Ei und es wird als Durchgeknalltes <%= potText(locale) %> Haustier schlüpfen.",
|
||||
"wackyPotionNotes": "Schütte dies über ein Ei und es wird als Durchgeknalltes <%= potText %> Haustier schlüpfen.",
|
||||
"questEggPlatypusText": "Schnabeltier",
|
||||
"questEggPlatypusMountText": "Schnabeltier",
|
||||
"questEggPlatypusAdjective": "ein Perfektionist",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"dontDespair": "Nicht verzweifeln!",
|
||||
"deathPenaltyDetails": "Du hast ein Level, Dein Gold und ein Stück Ausrüstung verloren, aber Du kannst alles durch harte Arbeit wieder zurückbekommen! Viel Glück -- Du schaffst das.",
|
||||
"refillHealthTryAgain": "Lebenspunkte wiederherstellen & Nochmal versuchen",
|
||||
"dyingOftenTips": "Passiert das öfters? <a href='https://habitica.fandom.com/wiki/Death_Mechanics#Strategies_for_Staying_Alive' target='_blank'>Hier gibt es einige Tipps!</a>",
|
||||
"dyingOftenTips": "Passiert das öfters? <a href='/static/faq#prevent-damage' target='_blank'>Hier gibt es einige Tipps!</a>",
|
||||
"losingHealthWarning": "Vorsicht - Du verlierst Lebenspunkte!",
|
||||
"losingHealthWarning2": "Lass Deine Lebenspunkte nicht auf null fallen! Sollte das passieren wirst Du ein Level, Dein Gold und einen Gegenstand Deiner Ausrüstung verlieren.",
|
||||
"toRegainHealth": "Um Deine Lebenspunkte wiederherzustellen:",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user