mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 05:07:22 +01:00
Compare commits
13 Commits
phillip/gu
...
phillip/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6092cba1ea | ||
|
|
0b4adbbf07 | ||
|
|
8faa5b0582 | ||
|
|
95494c685b | ||
|
|
10978d46ab | ||
|
|
447eb6a0c4 | ||
|
|
3dec49b72c | ||
|
|
472d03f276 | ||
|
|
fd9a27c3ab | ||
|
|
a5c1423837 | ||
|
|
e9829b8b60 | ||
|
|
7ecb83dc7e | ||
|
|
e8ffe2286c |
@@ -1,12 +1,13 @@
|
||||
import gulp from 'gulp';
|
||||
import clean from 'rimraf';
|
||||
import { rimraf as clean } from 'rimraf';
|
||||
import apidoc from 'apidoc';
|
||||
|
||||
const APIDOC_DEST_PATH = './apidoc/html';
|
||||
const APIDOC_SRC_PATH = './website/server';
|
||||
const APIDOC_CONFIG_PATH = './apidoc/apidoc.json';
|
||||
gulp.task('apidoc:clean', done => {
|
||||
clean(APIDOC_DEST_PATH, done);
|
||||
clean.sync(APIDOC_DEST_PATH);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('apidoc', gulp.series('apidoc:clean', done => {
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 clean from 'rimraf';
|
||||
import { rimraf as clean } from 'rimraf';
|
||||
|
||||
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
|
||||
.pipe(babel())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import gulp from 'gulp';
|
||||
import spritesmith from 'gulp.spritesmith';
|
||||
import clean from 'rimraf';
|
||||
import { rimraf as clean } from 'rimraf';
|
||||
import mergeStream from 'merge-stream';
|
||||
import { sync } from 'glob';
|
||||
|
||||
@@ -109,7 +109,8 @@ gulp.task('sprites:main', async () => {
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', done => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
clean.sync(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`);
|
||||
done();
|
||||
});
|
||||
|
||||
gulp.task('sprites:compile', gulp.series('sprites:clean', 'sprites:main', done => done()));
|
||||
|
||||
Submodule habitica-images updated: e3215f16f9...aa72332019
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.40.1",
|
||||
"version": "5.41.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.40.1",
|
||||
"version": "5.41.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -6070,9 +6070,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -11431,9 +11432,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.40.1",
|
||||
"version": "5.41.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -29,9 +29,9 @@
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-habitrpg": "^6.2.3",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.21.1",
|
||||
"express": "5.1.0",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"express-validator": "7.2.1",
|
||||
"firebase-admin": "^12.1.1",
|
||||
"glob": "^8.1.0",
|
||||
"got": "^11.8.6",
|
||||
@@ -66,8 +66,8 @@
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"rimraf": "6.0.1",
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -234,7 +234,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
@@ -254,7 +254,7 @@ describe('emails', () => {
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
|
||||
@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
|
||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||
});
|
||||
|
||||
it('highlights users with case-insensitive matching', async () => {
|
||||
const text = '@USER: message @User2 @USER3';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
|
||||
});
|
||||
|
||||
it('doesn\'t highlight nonexisting users', async () => {
|
||||
const text = '@nouser message';
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let iapIsCanceledStub;
|
||||
let iapIsExpiredStub;
|
||||
let paymentBuySkuStub;
|
||||
let iapGetPurchaseDataStub;
|
||||
let validateGiftMessageStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if no active subscription is found', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||
.returns(true);
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: 'subscription1month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let paymentBuySkuStub;
|
||||
let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.isExpired.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
const laterDate = moment.utc().add(7, 'days');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
.returns([{
|
||||
startTimeMillis: expirationDate.valueOf(),
|
||||
expirationDate,
|
||||
autoRenewing: false,
|
||||
}, {
|
||||
startTimeMillis: laterDate.valueOf(),
|
||||
expirationDate: laterDate,
|
||||
autoRenewing: false,
|
||||
},
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ autoRenewing: true }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ expirationDate, autoRenewing: false }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -238,6 +238,18 @@ describe('POST /chat', () => {
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with case-insensitive mentions', async () => {
|
||||
const originalUsername = member.auth.local.username;
|
||||
const uppercaseUsername = originalUsername.toUpperCase();
|
||||
const messageWithMentions = `hi @${uppercaseUsername}`;
|
||||
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
|
||||
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||
|
||||
expect(newMessage.message.id).to.exist;
|
||||
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with a max length of 3000 chars', async () => {
|
||||
const veryLongMessage = `
|
||||
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
|
||||
|
||||
38
website/client/package-lock.json
generated
38
website/client/package-lock.json
generated
@@ -38,7 +38,7 @@
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
@@ -5123,6 +5123,20 @@
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -7126,6 +7140,21 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/popper.js": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
@@ -8528,9 +8557,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.3.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
|
||||
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"timers-browserify": "^2.0.12",
|
||||
"uuid": "^9.0.1",
|
||||
"validator": "^13.9.0",
|
||||
"vite": "^6.0.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
|
||||
9
website/client/src/assets/images/gifts_bg.svg
Normal file
9
website/client/src/assets/images/gifts_bg.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="378" height="176" viewBox="0 0 378 176" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0H378V174C378 175.105 377.105 176 376 176H1.99999C0.895423 176 0 175.105 0 174V0Z" fill="url(#paint0_linear_2257_239)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2257_239" x1="378" y1="0" x2="0" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#925CF3"/>
|
||||
<stop offset="1" stop-color="#34B5C1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
37
website/client/src/assets/images/gifts_start.svg
Normal file
37
website/client/src/assets/images/gifts_start.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="48" height="96" viewBox="0 0 48 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.10104 12.0483C-2.82088 9.43721 -3.53422 6.57214 -5.6115 5.24584C-7.68877 3.91954 -9.89543 4.92709 -10.1422 6.808C-10.3891 8.68891 -9.06061 9.83066 -4.97737 13.9337C-3.81821 15.0985 -3.3812 14.6594 -3.10104 12.0483Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.34089 15.2054C4.45116 13.6561 7.27707 12.8443 9.45877 13.9889C11.6405 15.1334 11.8754 17.5575 10.3778 18.7127C8.88016 19.868 7.23193 19.2828 1.65411 17.781C0.0706697 17.3546 0.230624 16.7548 2.34089 15.2054Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.549002 12.0098C-3.61871 9.59194 -3.87667 15.8322 -2.20457 16.8023C-0.532473 17.7724 4.71671 14.4277 0.549002 12.0098Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L13.637 24.9825L9.18965 32.7229L-6.21656 23.785L-1.76917 16.0445Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.90457 13.0652L3.36623 19.0238L-1.08116 26.7643L-11.352 20.8057L-6.90457 13.0652Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L3.36623 19.0238L1.88377 21.604L-3.25163 18.6247L-1.76917 16.0445Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.21656 23.785L6.62195 31.2333L-3.75529 49.2944L-16.5938 41.8461L-6.21656 23.785Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.64886 25.2747L6.62195 31.2333L5.13948 33.8134L-5.13132 27.8548L-3.64886 25.2747Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.401307 24.1842L10.6721 30.1428L9.18965 32.7229L-1.08116 26.7643L0.401307 24.1842Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7924 38.4607L17.9387 42.0519L21.31 40.5834L24.8838 41.4413L23.4225 38.0537L24.2762 34.4625L20.9049 35.9309L17.3311 35.0731L18.7924 38.4607Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.93867 71.2331L-4.79238 74.8243L-1.42111 73.3559L2.15271 74.2137L0.691383 70.8261L1.54509 67.2349L-1.82618 68.7033L-5.4 67.8455L-3.93867 71.2331Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8949 25.3807L35.0583 29.8802L37.9424 26.2452L42.4202 25.0761L38.8028 22.178L37.6393 17.6786L34.7552 21.3135L30.2775 22.4826L33.8949 25.3807Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L40.579 68.1435L45.9507 88.2881L31.6312 92.1436L26.2596 71.999Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L26.2589 71.9966L31.6273 92.1421L17.3084 96L11.9401 75.8545Z" fill="#DDF3F3"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 23.3957 72.7701)" fill="#FFA624"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 26.2596 71.999)" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.9999 90.0369L30.8638 89.2658L31.6312 92.1436L28.7673 92.9147L27.9999 90.0369Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3957 72.7701L26.2596 71.999L27.0269 74.8768L24.163 75.6479L23.3957 72.7701Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L23.3951 72.7682L24.162 75.6461L12.707 78.7325L11.9401 75.8545Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5443 93.1213L27.9999 90.0369L28.7673 92.9147L17.3117 95.9991L16.5443 93.1213Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.1235 71.2279L40.579 68.1435L41.3464 71.0213L29.8908 74.1057L29.1235 71.2279Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.7277 88.4947L45.1833 85.4103L45.9507 88.2881L34.4951 91.3725L33.7277 88.4947Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.8638 89.2658L33.7277 88.4947L34.4951 91.3725L31.6312 92.1436L30.8638 89.2658Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L29.1235 71.2279L29.8908 74.1057L27.0269 74.8768L26.2596 71.999Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5224 56.3076C25.8087 53.7812 24.0792 51.3933 21.6588 50.9455C19.2383 50.4977 17.5679 52.2625 18.0403 54.0994C18.5126 55.9363 20.17 56.4948 25.4855 58.7621C26.9945 59.4057 27.236 58.834 26.5224 56.3076Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.745 57.1864C34.124 54.9555 36.4415 53.1391 38.8911 53.3791C41.3406 53.6191 42.4621 55.7782 41.5042 57.413C40.5463 59.0479 38.7999 59.1258 33.0684 59.8329C31.4413 60.0337 31.366 59.4173 32.745 57.1864Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8923 54.898C25.1267 54.225 27.2139 60.108 29.1258 60.378C31.0378 60.648 34.6579 55.571 29.8923 54.898Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L46.8635 61.9994L45.6255 70.8503L28.0091 68.3625L29.247 59.5115Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6306 57.0236L29.247 59.5114L28.0091 68.3624L10.3927 65.8745L11.6306 57.0236Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L35.1192 60.3408L33.8813 69.1917L22.137 67.5332L23.3749 58.6822Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.0091 68.3625L22.137 67.5332L23.3749 58.6822Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L35.1192 60.3408L34.7065 63.2911L28.8344 62.4618L29.247 59.5115Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.8344 62.4618L22.9622 61.6326L23.3749 58.6822Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8053 62.9241L22.5496 64.5827L22.137 67.533L10.3927 65.8745L10.8053 62.9241Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2939 66.2414L46.0382 67.9L45.6255 70.8503L33.8813 69.1917L34.2939 66.2414Z" fill="#DDF3F3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
@@ -30,12 +30,23 @@
|
||||
cursor: default;
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
background-color: $gray-700;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba($black, 0.12),
|
||||
0 1px 2px 0 rgba($black, 0.24);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 4px 12px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
.svg {
|
||||
color: $gray-300;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ h4 {
|
||||
background-color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
background-color: $yellow-50 !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: $white !important;
|
||||
}
|
||||
|
||||
8
website/client/src/assets/svg/close-white.svg
Normal file
8
website/client/src/assets/svg/close-white.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="#FFFFFF" fill-rule="nonzero">
|
||||
<polygon points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.1792 31.6843L46.8536 22.3769L23.918 28.6988L18.861 42.5218L44.341 58.5813L58.1792 31.6843Z" fill="#FF944C"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L46.1108 26.1328L36.2812 28.8422L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M30.2393 39.0304L26.4518 31.5515L36.2813 28.8422L30.2393 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L36.2813 28.8422L30.2393 39.0304L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.1108 26.1328L46.6218 34.5148L53.8301 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L26.4518 31.5516L30.2393 39.0304L23.0309 41.0173Z" fill="#FA8537"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.6218 34.5148L43.0424 53.79L53.8301 32.5279Z" fill="#FA8537"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L30.2393 39.0304L43.0425 53.79L23.0309 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L30.2393 39.0304L43.0425 53.79L46.6218 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.555 4.15937L47.026 0.420004L38.7773 1.59601L36.4144 6.17539L44.5675 12.8919L50.555 4.15937Z" fill="#FFBE5D"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L46.6034 1.6924L43.0682 2.1964L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M40.5221 5.46854L39.5331 2.7004L43.0682 2.1964L40.5221 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L43.0683 2.1964L40.5221 5.46855L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25894L46.6034 1.6924L46.414 4.62854L49.0064 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M37.9296 5.83815L39.5331 2.70041L40.5221 5.46855L37.9296 5.83815Z" fill="#FFA624"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25893L46.414 4.62853L44.3259 11.1688L49.0064 4.25893Z" fill="#FFA624"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M37.9297 5.83815L40.5221 5.46855L44.326 11.1688L37.9297 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L40.5221 5.46855L44.326 11.1688L46.414 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2986 16.7775L24.6513 8.36623L11.1016 3.94533L4.07056 9.19883L11.614 25.6769L27.2986 16.7775Z" fill="#FF6165"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L23.0573 10.0026L17.2502 8.10789L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M10.908 11.2141L11.4432 6.21322L17.2502 8.10789L10.908 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L17.2502 8.10789L10.9081 11.2141L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L23.0573 10.0026L20.5864 14.3719L24.8449 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M6.64955 9.82464L11.4432 6.21321L10.908 11.2141L6.64955 9.82464Z" fill="#F23035"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L20.5864 14.3719L12.5221 22.8464L24.8449 15.7613Z" fill="#F23035"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.64959 9.82464L10.9081 11.2141L12.5221 22.8463L6.64959 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L10.9081 11.2141L12.5221 22.8463L20.5864 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.82083 31.6843L17.1464 22.3769L40.082 28.6988L45.139 42.5218L19.659 58.5813L5.82083 31.6843Z" fill="#24CC8F"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L17.8892 26.1328L27.7188 28.8422L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.7607 39.0304L37.5482 31.5515L27.7187 28.8422L33.7607 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L27.7187 28.8422L33.7607 39.0304L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.8892 26.1328L17.3782 34.5148L10.1699 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L37.5482 31.5516L33.7607 39.0304L40.9691 41.0173Z" fill="#1CA372"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.3782 34.5148L20.9576 53.79L10.1699 32.5279Z" fill="#1CA372"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L33.7607 39.0304L20.9575 53.79L40.9691 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L33.7607 39.0304L20.9575 53.79L17.3782 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.445 4.15937L16.974 0.420004L25.2227 1.59601L27.5856 6.17539L19.4325 12.8919L13.445 4.15937Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L17.3966 1.6924L20.9318 2.1964L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M23.4779 5.46854L24.4669 2.7004L20.9318 2.1964L23.4779 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L20.9317 2.1964L23.4779 5.46855L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25894L17.3966 1.6924L17.586 4.62854L14.9936 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M26.0704 5.83815L24.4669 2.70041L23.4779 5.46855L26.0704 5.83815Z" fill="#4F2A93"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25893L17.586 4.62853L19.6741 11.1688L14.9936 4.25893Z" fill="#4F2A93"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M26.0703 5.83815L23.4779 5.46855L19.674 11.1688L26.0703 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L23.4779 5.46855L19.674 11.1688L17.586 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7014 16.7775L39.3487 8.36623L52.8984 3.94533L59.9294 9.19883L52.386 25.6769L36.7014 16.7775Z" fill="#50B5E9"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L40.9427 10.0026L46.7498 8.10789L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M53.092 11.2141L52.5568 6.21322L46.7498 8.10789L53.092 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L46.7498 8.10789L53.0919 11.2141L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L40.9427 10.0026L43.4136 14.3719L39.1551 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L52.5568 6.21321L53.092 11.2141L57.3504 9.82464Z" fill="#46A7D9"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L43.4136 14.3719L51.4779 22.8464L39.1551 15.7613Z" fill="#46A7D9"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L53.0919 11.2141L51.4779 22.8463L57.3504 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L53.0919 11.2141L51.4779 22.8463L43.4136 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -117,7 +117,7 @@ export default {
|
||||
closeWithAction () {
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
this.$router.push({ name: 'achievements' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||
}, 200);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||
this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||
this.isSearching = false;
|
||||
if (users.length === 1) {
|
||||
this.loadUser(users[0]._id);
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="form col-12">
|
||||
<button
|
||||
class="btn btn-danger mt-3 float-right"
|
||||
@click="confirmDeleteHero"
|
||||
>
|
||||
Begin Member deletion
|
||||
</button>
|
||||
<basic-details
|
||||
:user-id="hero._id"
|
||||
:auth="hero.auth"
|
||||
@@ -96,6 +102,53 @@
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
<b-modal
|
||||
id="delete-member-modal"
|
||||
title="Delete Member"
|
||||
ok-title="Delete"
|
||||
ok-variant="danger"
|
||||
cancel-title="Cancel"
|
||||
@ok="deleteHero"
|
||||
>
|
||||
<b-modal-body>
|
||||
<p>
|
||||
Are you sure you want to delete this member?
|
||||
</p>
|
||||
<p class="errorMessage">
|
||||
Please note: This action cannot be undone!
|
||||
</p>
|
||||
<div class="ml-4">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAccountCheck"
|
||||
v-model="deleteHabiticaAccount"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAccountCheck"
|
||||
>
|
||||
Delete Habitica account
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAmplitudeCheck"
|
||||
v-model="deleteAmplitudeData"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAmplitudeCheck"
|
||||
>
|
||||
Delete Amplitude data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal-body>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,6 +237,8 @@ export default {
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
deleteHabiticaAccount: true,
|
||||
deleteAmplitudeData: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -249,6 +304,25 @@ export default {
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
confirmDeleteHero () {
|
||||
if (this.hero._id === this.user._id) {
|
||||
window.alert('You cannot delete your own account.');
|
||||
return;
|
||||
}
|
||||
this.$root.$emit('bv::show::modal', 'delete-member-modal');
|
||||
},
|
||||
deleteHero () {
|
||||
this.$store.dispatch('hall:deleteHero', {
|
||||
uuid: this.hero._id,
|
||||
deleteHabiticaAccount: this.deleteHabiticaAccount,
|
||||
deleteAmplitudeData: this.deleteAmplitudeData,
|
||||
}).then(() => {
|
||||
this.$root.$emit('bv::hide::modal', 'delete-member-modal');
|
||||
this.$router.push({ name: 'adminPanel' });
|
||||
}).catch(err => {
|
||||
window.alert(err);
|
||||
});
|
||||
},
|
||||
hasUnsavedChanges (...comparisons) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
<router-link
|
||||
:to="{'name': 'groupAdminGroup', 'params': {'groupId': groupPartyData._id}}"
|
||||
>
|
||||
{{ groupPartyData._id }}
|
||||
</router-link>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
||||
@@ -15,6 +15,25 @@
|
||||
:class="{ 'open': expand }"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<span
|
||||
v-if="isSubscribed() && !isCancelled()"
|
||||
class="text-success float-right ml-3"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
<span
|
||||
v-else-if="isSubscribed() && isCancelled()"
|
||||
class="text-success float-right ml-3"
|
||||
>
|
||||
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
|
||||
class="text-warning float-right ml-3"
|
||||
>
|
||||
Inactive
|
||||
</span>
|
||||
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
@@ -46,7 +65,7 @@
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">
|
||||
<option value="Group Plan">
|
||||
Group Plan
|
||||
</option>
|
||||
<option value="Stripe">
|
||||
@@ -154,7 +173,11 @@
|
||||
>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">
|
||||
{{ group.name }}
|
||||
<router-link
|
||||
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
|
||||
>
|
||||
{{ group.name }}
|
||||
</router-link>
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
@@ -245,8 +268,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<small
|
||||
v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId"
|
||||
v-if="isSubscribed() && !isCancelled()"
|
||||
class="text-success"
|
||||
>
|
||||
The subscription does not have a termination date and is active.
|
||||
@@ -419,6 +441,79 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<h2>Payment Details</h2>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="getSubscriptionPaymentDetails"
|
||||
>
|
||||
Get Subscription Payment Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="paymentDetails"
|
||||
>
|
||||
<div
|
||||
v-for="(value, key) in paymentDetails"
|
||||
:key="key"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
{{ getHumanReadablePaymentDetails(key).label }}:
|
||||
<span
|
||||
:id="`${key}_tooltip`"
|
||||
v-b-tooltip.hover.right="getHumanReadablePaymentDetails(key).help"
|
||||
class="info-icon"
|
||||
>?</span>
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="value === true">Yes</span>
|
||||
<span v-else-if="value === false">No</span>
|
||||
<span
|
||||
v-else-if="value instanceof String && isDate(value)"
|
||||
v-b-tooltip.hover="value"
|
||||
>
|
||||
{{ formatDate(value) }}
|
||||
</span>
|
||||
<span v-else-if="value === null">---</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<a
|
||||
v-if="hero.purchased.plan.paymentMethod === 'Google'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="playOrdersUrl"
|
||||
>
|
||||
Play Console
|
||||
</a>
|
||||
<a
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
|
||||
>
|
||||
PayPal Dashboard
|
||||
</a>
|
||||
<a
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
|
||||
>
|
||||
Stripe Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
@@ -474,17 +569,36 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
.form-group {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 0.8rem;
|
||||
color: $purple-400;
|
||||
cursor: pointer;
|
||||
margin-left: 0.2rem;
|
||||
background-color: $gray-500;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
background-color: $purple-400;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -495,6 +609,55 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
label: 'Customer ID',
|
||||
help: 'The unique identifier for the customer in the payment system.',
|
||||
},
|
||||
purchaseDate: {
|
||||
label: 'Purchase Date',
|
||||
help: 'The date when the subscription was purchased or renewed.',
|
||||
},
|
||||
originalPurchaseDate: {
|
||||
label: 'Original Purchase Date',
|
||||
help: 'The date when the subscription was first purchased.',
|
||||
},
|
||||
productId: {
|
||||
label: 'Product ID',
|
||||
help: 'The identifier for the product associated with the subscription.',
|
||||
},
|
||||
transactionId: {
|
||||
label: 'Transaction ID',
|
||||
help: 'The unique identifier for the last transaction in the payment system.',
|
||||
},
|
||||
isCanceled: {
|
||||
label: 'Is Canceled',
|
||||
help: 'Indicates whether the subscription has been canceled by the user or the system.',
|
||||
},
|
||||
isExpired: {
|
||||
label: 'Is Expired',
|
||||
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
|
||||
},
|
||||
expirationDate: {
|
||||
label: 'Termination Date',
|
||||
help: 'The date when the subscription will expire or has expired.',
|
||||
},
|
||||
nextPaymentDate: {
|
||||
label: 'Next Payment Date',
|
||||
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
|
||||
},
|
||||
lastPaymentDate: {
|
||||
label: 'Last Payment Date',
|
||||
help: 'The date when the lastpayment was made for the subscription.',
|
||||
},
|
||||
failedPayments: {
|
||||
label: 'Failed Payments',
|
||||
help: 'Number of times the payment failed for this subscription.',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
@@ -520,6 +683,7 @@ export default {
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
paymentDetails: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -553,6 +717,9 @@ export default {
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
playOrdersUrl () {
|
||||
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -583,6 +750,20 @@ export default {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
getSubscriptionPaymentDetails () {
|
||||
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
|
||||
.then(details => {
|
||||
if (details) {
|
||||
this.paymentDetails = details;
|
||||
} else {
|
||||
alert('No payment details found.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching subscription payment details:', error);
|
||||
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
|
||||
});
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
@@ -601,6 +782,31 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
getHumanReadablePaymentDetails (key) {
|
||||
return humanReadablePaymentDetails[key] || { label: key, help: '' };
|
||||
},
|
||||
isDate (date) {
|
||||
return moment(date).isValid();
|
||||
},
|
||||
formatDate (date) {
|
||||
return date ? moment(date).format('MM/DD/YYYY') : '---';
|
||||
},
|
||||
isSubscribed () {
|
||||
console.log(this.hero.purchased.plan.customerId, this.hero.purchased.plan.dateTerminated);
|
||||
return this.hero.purchased.plan
|
||||
&& this.hero.purchased.plan.customerId
|
||||
&& this.hero.purchased.plan.planId
|
||||
&& this.hero.purchased.plan.paymentMethod
|
||||
&& (
|
||||
!this.hero.purchased.plan.dateTerminated
|
||||
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
|
||||
);
|
||||
},
|
||||
isCancelled () {
|
||||
return this.hero.purchased.plan
|
||||
&& this.hero.purchased.plan.dateTerminated
|
||||
&& this.hero.purchased.plan.dateTerminated !== '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -226,7 +226,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async retrieveUserHistory () {
|
||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||
const history = await this.$store.dispatch('admin:getUserHistory', { userIdentifier: this.hero._id });
|
||||
this.armoire = history.armoire;
|
||||
this.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
>
|
||||
{{ $t('adminPanel') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'groupSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'groupAdmin'}"
|
||||
>
|
||||
{{ $t('groupAdmin') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'accessControl')"
|
||||
class="nav-link"
|
||||
|
||||
47
website/client/src/components/admin/formRow.vue
Normal file
47
website/client/src/components/admin/formRow.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-3 col-form-label"><slot name="label">{{ label }}</slot></label>
|
||||
<div class="col-sm-9">
|
||||
<slot>
|
||||
<textarea
|
||||
v-if="inputType === 'textarea'"
|
||||
:value="value"
|
||||
class="form-control"
|
||||
:rows="rows"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
></textarea>
|
||||
<input
|
||||
v-else
|
||||
:value="value"
|
||||
class="form-control"
|
||||
:type="inputType"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'input',
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
type: [String, Boolean],
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
rows: {
|
||||
default: 3,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<form>
|
||||
<form-row
|
||||
v-model="group.name"
|
||||
:label="$t('groupName')"
|
||||
/>
|
||||
<form-row
|
||||
v-model="group.summary"
|
||||
:label="$t('guildSummary')"
|
||||
input-type="textarea"
|
||||
/>
|
||||
<form-row
|
||||
v-model="group.description"
|
||||
:label="$t('groupDescription')"
|
||||
input-type="textarea"
|
||||
rows="6"
|
||||
/>
|
||||
<form-row
|
||||
v-model="group.bannedWordsAllowed"
|
||||
:label="$t('bannedWordsAllowed')"
|
||||
input-type="checkbox"
|
||||
/>
|
||||
<form-row
|
||||
v-model="group.leaderOnly.challenges"
|
||||
:label="$t('leaderOnlyChallenges')"
|
||||
input-type="checkbox"
|
||||
/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formRow from '@/components/admin/formRow.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
formRow,
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div v-if="hasPermission(user, 'groupSupport')">
|
||||
<h2>{{ group.name }}</h2>
|
||||
<supportContainer
|
||||
:title="$t('groupData')"
|
||||
>
|
||||
<groupData
|
||||
:group="group"
|
||||
/>
|
||||
</supportContainer>
|
||||
<supportContainer
|
||||
:title="$t('groupPlanSubscription')"
|
||||
/>
|
||||
<supportContainer
|
||||
v-if="group.type === 'party'"
|
||||
:title="$t('questDetails')"
|
||||
/>
|
||||
<supportContainer
|
||||
:title="$t('members')"
|
||||
>
|
||||
<members
|
||||
:group="group"
|
||||
/>
|
||||
</supportContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { userStateMixin } from '../../../../mixins/userState';
|
||||
import supportContainer from '../../supportContainer.vue';
|
||||
import groupData from './groupData.vue';
|
||||
import members from './members.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
supportContainer,
|
||||
groupData,
|
||||
members,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
groupId: '',
|
||||
group: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
groupId () {
|
||||
this.loadGroup(this.groupId);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.groupId = this.$route.params.groupId;
|
||||
},
|
||||
methods: {
|
||||
clearData () {
|
||||
this.group = {};
|
||||
},
|
||||
async loadGroup (groupId) {
|
||||
this.$emit('changeGroupId', groupId);
|
||||
this.group = await this.$store.dispatch('admin:getGroup', { groupId });
|
||||
},
|
||||
async updateGroup () {
|
||||
await this.$store.dispatch('admin:updateGroup', { group: this.group });
|
||||
this.$emit('groupSaved', this.group);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<form-row
|
||||
:label="$t('groupLeader')"
|
||||
>
|
||||
<strong class="col-form-label">
|
||||
<router-link
|
||||
:to="{'name': 'adminPanelUser', 'params': {'userIdentifier': group.leader }}"
|
||||
>
|
||||
{{ group.leader }}
|
||||
</router-link>
|
||||
</strong>
|
||||
</form-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import formRow from '@/components/admin/formRow.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
formRow,
|
||||
},
|
||||
props: {
|
||||
group: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
93
website/client/src/components/admin/groups/index.vue
Normal file
93
website/client/src/components/admin/groups/index.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="row standard-page col-12 d-flex justify-content-center">
|
||||
<div class="group-admin-content">
|
||||
<h1>{{ $t("groupAdmin") }}</h1>
|
||||
<form
|
||||
class="form-inline"
|
||||
@submit.prevent="loadGroup(groupID)"
|
||||
>
|
||||
<div class="input-group col pl-0 pr-0">
|
||||
<input
|
||||
v-model="groupID"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="Group ID"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="button"
|
||||
:disabled="!groupID"
|
||||
@click="loadGroup(groupID)"
|
||||
>
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<router-view
|
||||
class="mt-3"
|
||||
@changeGroupId="changeGroupId"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.uidField {
|
||||
min-width: 45ch;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
width:auto;
|
||||
}
|
||||
|
||||
.group-admin-content {
|
||||
flex: 0 0 800px;
|
||||
max-width: 800px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import VueRouter from 'vue-router';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
const { isNavigationFailure, NavigationFailureType } = VueRouter;
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
groupID: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('groupAdmin'),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
changeGroupId (id) {
|
||||
this.groupID = id;
|
||||
},
|
||||
async loadGroup (groupId) {
|
||||
if (this.$router.currentRoute.name === 'groupAdminGroup') {
|
||||
await this.$router.push({
|
||||
name: 'groupAdmin',
|
||||
});
|
||||
}
|
||||
await this.$router.push({
|
||||
name: 'groupAdminGroup',
|
||||
params: { groupId },
|
||||
}).catch(failure => {
|
||||
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
|
||||
this.$router.go();
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
53
website/client/src/components/admin/supportContainer.vue
Normal file
53
website/client/src/components/admin/supportContainer.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
<h3
|
||||
class="mb-0 mt-0"
|
||||
:class="{'open': expand}"
|
||||
@click="expand = !expand"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
class="card-body"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand && onSave"
|
||||
class="card-footer"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary mt-1"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
onSave: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
expand: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -72,32 +72,40 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-right">
|
||||
<div
|
||||
class="box member-count"
|
||||
class="box member-count p-2"
|
||||
@click="showMemberModal()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon member-icon"
|
||||
v-html="icons.memberIcon"
|
||||
></div>
|
||||
{{ challenge.memberCount }}
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('participantsTitle') }}
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
class="svg-icon member-icon"
|
||||
v-html="icons.memberIcon"
|
||||
></div>
|
||||
<span class="number">{{ challenge.memberCount }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('participantsTitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div
|
||||
class="svg-icon gem-icon"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
{{ challenge.prize || 0 }}
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('prize') }}
|
||||
<div class="box prize-count p-2">
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
class="svg-icon gem-icon"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
<span class="number">{{ challenge.prize || 0 }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('prize') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +312,6 @@
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
padding: 1em;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
@@ -314,22 +321,88 @@
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.member-count:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-number-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.1em;
|
||||
|
||||
.number {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
margin-right: .2em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 12px;
|
||||
margin-top: 0.4em;
|
||||
color: $gray-200;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
line-height: 1.15;
|
||||
word-break: break-word;
|
||||
max-height: 2.3em;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&.member-count {
|
||||
.icon-number-row {
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
max-height: 2.2em;
|
||||
}
|
||||
}
|
||||
|
||||
&.prize-count {
|
||||
.icon-number-row {
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
max-height: 2.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
id="close-challenge-modal"
|
||||
:title="$t('endChallenge')"
|
||||
size="md"
|
||||
:hide-header="false"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
@@ -15,6 +16,9 @@
|
||||
>
|
||||
{{ $t('endChallenge') }}
|
||||
</h2>
|
||||
<close-x
|
||||
@close="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
|
||||
/>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<span
|
||||
@@ -28,28 +32,67 @@
|
||||
class="col-12"
|
||||
>
|
||||
<div class="col-12">
|
||||
<div class="support-habitica">
|
||||
<!-- @TODO: Add challenge achievement badge here-->
|
||||
<div class="badge-section">
|
||||
<div
|
||||
class="gems-left"
|
||||
v-html="icons.gemsOrange"
|
||||
></div>
|
||||
<div
|
||||
class="challenge-badge"
|
||||
v-html="icons.endChallengeBadge"
|
||||
></div>
|
||||
<div
|
||||
class="gems-right"
|
||||
v-html="icons.gemsPurple"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<member-search-dropdown
|
||||
:text="winnerText"
|
||||
:members="members"
|
||||
:challenge-id="challengeId"
|
||||
@member-selected="selectMember"
|
||||
/>
|
||||
<div class="col-12 search-input-container">
|
||||
<div class="search-input-wrapper">
|
||||
<div
|
||||
class="search-icon"
|
||||
v-html="icons.search"
|
||||
></div>
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="@Username"
|
||||
@input="searchMembers"
|
||||
@focus="showResults = true"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<div
|
||||
v-if="showResults && filteredMembers.length > 0"
|
||||
class="search-results"
|
||||
>
|
||||
<div
|
||||
v-for="member in filteredMembers"
|
||||
:key="member._id"
|
||||
class="search-result-item"
|
||||
@mousedown="selectMember(member)"
|
||||
>
|
||||
{{ getMemberDisplayName(member) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
class="btn award-winner-btn"
|
||||
:class="{'has-winner': winner._id}"
|
||||
:disabled="!winner._id"
|
||||
@click="closeChallenge"
|
||||
>
|
||||
{{ $t('awardWinners') }}
|
||||
<span>{{ $t('awardWinners') }}</span>
|
||||
<div
|
||||
class="gem-icon"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
@@ -60,14 +103,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
||||
<strong
|
||||
v-once
|
||||
class="delete-challenge-text"
|
||||
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="col-12 refund-text"
|
||||
>
|
||||
{{ $t('deleteChallengeRefundDescription') }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-danger"
|
||||
class="btn btn-danger delete-challenge-btn"
|
||||
@click="deleteChallenge()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon color delete-icon"
|
||||
v-html="icons.deleteIcon"
|
||||
></div>
|
||||
{{ $t('deleteChallenge') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -82,6 +138,7 @@
|
||||
|
||||
<style lang='scss'>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/button.scss';
|
||||
|
||||
#close-challenge-modal {
|
||||
h2 {
|
||||
@@ -94,26 +151,190 @@
|
||||
|
||||
.header-wrap {
|
||||
width: 100%;
|
||||
padding-top: 2em;
|
||||
padding-top: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.support-habitica {
|
||||
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
|
||||
width: 325px;
|
||||
height: 89px;
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
margin-top: 1em !important;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-55%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $gray-200;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding-left: 36px;
|
||||
padding-right: 12px;
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, border-width 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: $white;
|
||||
border: 1px solid $gray-400;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-result-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: $purple-600;
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-challenge-text {
|
||||
color: $maroon-50;
|
||||
}
|
||||
|
||||
.refund-text {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color: $gray-50;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.delete-challenge-btn {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.award-winner-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 4px 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:not(:disabled) {
|
||||
background-color: $white;
|
||||
color: $gray-200;
|
||||
border: 1px solid $gray-400;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
&.has-winner {
|
||||
background-color: $purple-200;
|
||||
color: $white;
|
||||
border-color: $purple-200;
|
||||
}
|
||||
|
||||
&:hover:not(.has-winner) {
|
||||
background-color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: $gems-color;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
margin: -24px auto 0;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.gems-left, .gems-right {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.challenge-badge {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer, .modal-header {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
margin-top: 2em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.col-12:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.or {
|
||||
@@ -123,21 +344,39 @@
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
font-weight: bold;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
|
||||
import searchIcon from '@/assets/svg/for-css/search.svg?raw';
|
||||
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||
import endChallengeBadge from '@/assets/svg/for-css/end_challenge_badge.svg?raw';
|
||||
import gemsOrange from '@/assets/svg/for-css/orange100_red100_yellow100_gems.svg?raw';
|
||||
import gemsPurple from '@/assets/svg/for-css/purple200_green10_blue100_gems.svg?raw';
|
||||
import closeX from '@/components/ui/closeX';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
closeX,
|
||||
},
|
||||
props: ['challengeId', 'members', 'prize', 'flagCount'],
|
||||
data () {
|
||||
return {
|
||||
winner: {},
|
||||
searchTerm: '',
|
||||
showResults: false,
|
||||
filteredMembers: [],
|
||||
icons: Object.freeze({
|
||||
search: searchIcon,
|
||||
deleteIcon,
|
||||
gem: gemIcon,
|
||||
endChallengeBadge,
|
||||
gemsOrange,
|
||||
gemsPurple,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -150,8 +389,35 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
searchMembers () {
|
||||
if (!this.searchTerm) {
|
||||
this.filteredMembers = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const searchLower = this.searchTerm.toLowerCase().replace('@', '');
|
||||
this.filteredMembers = this.members.filter(member => {
|
||||
const username = member.auth?.local?.username || '';
|
||||
const displayName = member.profile?.name || '';
|
||||
return username.toLowerCase().includes(searchLower)
|
||||
|| displayName.toLowerCase().includes(searchLower);
|
||||
}).slice(0, 10);
|
||||
},
|
||||
getMemberDisplayName (member) {
|
||||
if (member.auth?.local?.username) {
|
||||
return `@${member.auth.local.username}`;
|
||||
}
|
||||
return member.profile?.name || '';
|
||||
},
|
||||
selectMember (member) {
|
||||
this.winner = member;
|
||||
this.searchTerm = this.getMemberDisplayName(member);
|
||||
this.showResults = false;
|
||||
},
|
||||
handleBlur () {
|
||||
setTimeout(() => {
|
||||
this.showResults = false;
|
||||
}, 200);
|
||||
},
|
||||
async closeChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
class="notification d-flex flex-column justify-content-center text-center"
|
||||
class="notification d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<strong
|
||||
v-once
|
||||
class="mx-auto mb-2"
|
||||
<img
|
||||
src="@/assets/images/gifts_start.svg"
|
||||
class="gift-start"
|
||||
alt=""
|
||||
>
|
||||
{{ $t('g1g1') }}
|
||||
</strong>
|
||||
<small
|
||||
v-once
|
||||
class="mx-4 mb-3"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</small>
|
||||
<div
|
||||
class="btn-secondary mx-auto d-flex"
|
||||
@click="showSelectUser()"
|
||||
>
|
||||
<div
|
||||
<div class="content-wrapper d-flex flex-column justify-content-center text-center">
|
||||
<strong
|
||||
v-once
|
||||
class="m-auto"
|
||||
class="mx-auto mb-2"
|
||||
>
|
||||
{{ $t('g1g1') }}
|
||||
</strong>
|
||||
<small
|
||||
v-once
|
||||
class="mx-4 mb-3"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</small>
|
||||
<button
|
||||
class="btn btn-secondary mx-auto"
|
||||
@click="showSelectUser()"
|
||||
>
|
||||
{{ $t('sendGift') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src="@/assets/images/gifts_start.svg"
|
||||
class="gift-end"
|
||||
alt=""
|
||||
>
|
||||
<div
|
||||
class="notification-remove"
|
||||
@click.stop="remove()"
|
||||
class="close-x"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
class="svg-icon svg-close"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
@@ -41,51 +47,89 @@
|
||||
<style lang='scss' scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
small, strong {
|
||||
small {
|
||||
color: $white;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $white;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background-image: url('@/assets/images/g1g1-notif.png');
|
||||
background-image: url('@/assets/images/gifts_bg.svg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 10rem;
|
||||
padding: 3rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-remove {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 24px;
|
||||
top: 24px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 5.75rem;
|
||||
min-height: 1.5rem;
|
||||
border-radius: 2px;
|
||||
border-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
.gift-start {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gift-end {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scaleX(-1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
|
||||
&:hover .svg-close {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.svg-close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close-teal.svg?raw';
|
||||
import { mapActions } from '@/libs/store';
|
||||
import closeIcon from '@/assets/svg/close-white.svg?raw';
|
||||
|
||||
export default {
|
||||
props: ['notification'],
|
||||
props: ['notification', 'eventKey'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -94,11 +138,11 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
readNotification: 'notifications:readNotification',
|
||||
}),
|
||||
remove () {
|
||||
this.readNotification({ notificationId: this.notification.id });
|
||||
if (this.eventKey) {
|
||||
window.sessionStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
|
||||
}
|
||||
this.$emit('notification-removed');
|
||||
},
|
||||
showSelectUser () {
|
||||
this.$root.$emit('bv::show::modal', 'select-user-modal');
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'achievements' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'stats' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#stats`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
v-if="showOnboardingGuide"
|
||||
:never-seen="hasSpecialBadge"
|
||||
/>
|
||||
<gift-one-get-one-notification
|
||||
v-if="shouldShowG1g1"
|
||||
:notification="g1g1Notification"
|
||||
:event-key="g1g1EventKey"
|
||||
@notification-removed="handleG1g1Removed"
|
||||
/>
|
||||
<component
|
||||
:is="notification.type"
|
||||
v-for="notification in notifications"
|
||||
@@ -114,6 +120,7 @@
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
|
||||
import find from 'lodash/find';
|
||||
import { mapState, mapActions } from '@/libs/store';
|
||||
import notificationsIcon from '@/assets/svg/notifications.svg?raw';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
@@ -151,6 +158,7 @@ export default {
|
||||
CARD_RECEIVED,
|
||||
CHALLENGE_INVITATION,
|
||||
GIFT_ONE_GET_ONE,
|
||||
GiftOneGetOneNotification: GIFT_ONE_GET_ONE,
|
||||
GROUP_TASK_ASSIGNED,
|
||||
GROUP_TASK_CLAIMED,
|
||||
GROUP_TASK_NEEDS_WORK,
|
||||
@@ -178,17 +186,14 @@ export default {
|
||||
hasSpecialBadge: false,
|
||||
quests,
|
||||
openStatus: undefined,
|
||||
g1g1Hidden: false,
|
||||
actionableNotifications: [
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION',
|
||||
],
|
||||
// A list of notifications handled by this component,
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
// NOTE: Those not listed here won't be shown in the notification panel!
|
||||
handledNotifications: [
|
||||
'NEW_STUFF',
|
||||
'ITEM_RECEIVED',
|
||||
'GIFT_ONE_GET_ONE',
|
||||
'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION',
|
||||
'PARTY_INVITATION',
|
||||
@@ -207,7 +212,10 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
notificationsOrder () {
|
||||
// Returns a map of NOTIFICATION_TYPE -> POSITION
|
||||
const orderMap = {};
|
||||
@@ -286,9 +294,9 @@ export default {
|
||||
|
||||
return notifications;
|
||||
},
|
||||
// The total number of notification, shown inside the dropdown
|
||||
notificationsCount () {
|
||||
return this.notifications.length;
|
||||
const g1g1Count = this.shouldShowG1g1 ? 1 : 0;
|
||||
return this.notifications.length + g1g1Count;
|
||||
},
|
||||
hasUnseenNotifications () {
|
||||
return this.notifications.some(notification => (notification.seen === false));
|
||||
@@ -299,6 +307,30 @@ export default {
|
||||
showOnboardingGuide () {
|
||||
return !hasCompletedOnboarding(this.user);
|
||||
},
|
||||
currentG1g1Event () {
|
||||
return find(this.currentEventList, event => event.promo === 'g1g1');
|
||||
},
|
||||
g1g1EventKey () {
|
||||
if (!this.currentG1g1Event || !this.currentG1g1Event.start) return null;
|
||||
const startDate = new Date(this.currentG1g1Event.start);
|
||||
return `${startDate.getFullYear()}-${startDate.getMonth()}`;
|
||||
},
|
||||
shouldShowG1g1 () {
|
||||
if (!this.currentG1g1Event) return false;
|
||||
const eventKey = this.g1g1EventKey;
|
||||
if (eventKey && window.sessionStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
|
||||
return false;
|
||||
}
|
||||
return !this.g1g1Hidden;
|
||||
},
|
||||
g1g1Notification () {
|
||||
return {
|
||||
type: 'GIFT_ONE_GET_ONE',
|
||||
id: `g1g1-event-${this.currentG1g1Event?.start || 'default'}`,
|
||||
data: {},
|
||||
seen: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const onboardingPanelState = getLocalSetting(CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE);
|
||||
@@ -364,6 +396,9 @@ export default {
|
||||
isActionable (notification) {
|
||||
return this.actionableNotifications.indexOf(notification.type) !== -1;
|
||||
},
|
||||
handleG1g1Removed () {
|
||||
this.g1g1Hidden = true;
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -176,7 +176,12 @@ export default {
|
||||
}
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$router.push({ name: startingPage });
|
||||
const userId = this.$store.state.user.data._id;
|
||||
let path = `/profile/${userId}`;
|
||||
if (startingPage !== 'profile') {
|
||||
path += `#${startingPage}`;
|
||||
}
|
||||
this.$router.push(path);
|
||||
},
|
||||
toLearnMore () {
|
||||
this.$router.push({ name: 'subscription' });
|
||||
|
||||
@@ -454,17 +454,14 @@ export default {
|
||||
},
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
if (!username) return false;
|
||||
const usernamePattern = new RegExp(`@${escapeRegExp(username)}(?:\\b|(?=[^a-zA-Z0-9_]))`, 'i');
|
||||
message.highlight = usernamePattern.test(message.text);
|
||||
return message.highlight;
|
||||
},
|
||||
flagCountDescription () {
|
||||
|
||||
@@ -1126,7 +1126,12 @@ export default {
|
||||
this.loadUser();
|
||||
this.oldTitle = this.$store.state.title;
|
||||
this.handleExternalLinks();
|
||||
this.selectPage(this.startingPage);
|
||||
// Check if there's a hash in the URL to determine the starting page
|
||||
let pageToSelect = this.startingPage;
|
||||
if (window.location.hash && (window.location.hash === '#stats' || window.location.hash === '#achievements')) {
|
||||
pageToSelect = window.location.hash.substring(1);
|
||||
}
|
||||
this.selectPage(pageToSelect);
|
||||
this.$root.$on('habitica:report-profile-result', () => {
|
||||
this.loadUser();
|
||||
});
|
||||
@@ -1211,10 +1216,15 @@ export default {
|
||||
},
|
||||
selectPage (page) {
|
||||
this.selectedPage = page || 'profile';
|
||||
window.history.replaceState(null, null, '');
|
||||
const profileUserId = this.userId || this.userLoggedIn._id;
|
||||
let newPath = `/profile/${profileUserId}`;
|
||||
if (page !== 'profile') {
|
||||
newPath += `#${page}`;
|
||||
}
|
||||
window.history.replaceState(null, null, newPath);
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('user'),
|
||||
subSection: this.$t(this.startingPage),
|
||||
subSection: this.$t(page),
|
||||
});
|
||||
},
|
||||
getNextIncentive () {
|
||||
|
||||
@@ -19,7 +19,7 @@ let analyticsReady = false;
|
||||
function _getConsentedUser () {
|
||||
const store = getStore();
|
||||
const user = store.state.user.data;
|
||||
if (!user?.preferences?.analyticsConsent || navigator.globalPrivacyControl) {
|
||||
if (!user?.preferences?.analyticsConsent) {
|
||||
return false;
|
||||
}
|
||||
return user;
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import habiticaMarkdown from 'habitica-markdown/withMentions';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
export default function renderWithMentions (text, user) {
|
||||
if (!text) return null;
|
||||
const env = { userName: user.auth.local.username, displayName: user.profile.name };
|
||||
return habiticaMarkdown.render(String(text), env);
|
||||
const env = { userName: user.auth.local.username };
|
||||
let html = habiticaMarkdown.render(String(text), env);
|
||||
|
||||
if (user.auth.local.username) {
|
||||
const username = escapeRegExp(user.auth.local.username);
|
||||
const regex = new RegExp(`(<span class="at-text">@)(${username})(</span>)`, 'gi');
|
||||
html = html.replace(regex, (match, p1, p2, p3) => `${p1.replace('at-text', 'at-text at-highlight')}${p2}${p3}`);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 732px;
|
||||
flex: 0 0 751px;
|
||||
max-width: unset;
|
||||
|
||||
::v-deep {
|
||||
|
||||
@@ -33,6 +33,21 @@
|
||||
v-html="$t('privacySettingsOverview') + ' ' + $t('learnMorePrivacy')"
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
v-if="gpcEnabled"
|
||||
class="mx-4 px-3 py-2 mb-4 gpc-alert d-flex align-items-center black bg-yellow-50"
|
||||
>
|
||||
<div
|
||||
class="svg svg-icon mr-2"
|
||||
v-html="icons.alert"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="gpc-message"
|
||||
v-html="gpcInfo"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
@@ -91,6 +106,29 @@
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.gpc-alert {
|
||||
border-radius: 4px;
|
||||
line-height: 1.714;
|
||||
|
||||
.gpc-message {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
::v-deep a {
|
||||
color: $black;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 16px;
|
||||
opacity: 0.75;
|
||||
|
||||
::v-deep svg path {
|
||||
fill: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mb-28p {
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
@@ -110,6 +148,7 @@ import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
|
||||
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
|
||||
import { InlineSettingMixin } from '../components/inlineSettingMixin';
|
||||
import { mapState } from '@/libs/store';
|
||||
import alert from '@/assets/svg/for-css/alert.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
@@ -120,14 +159,32 @@ export default {
|
||||
SaveCancelButtons,
|
||||
ToggleSwitch,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
alert,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
gpcEnabled () {
|
||||
return navigator.globalPrivacyControl;
|
||||
},
|
||||
gpcInfo () {
|
||||
const gpcUrl = 'https://globalprivacycontrol.org/';
|
||||
if (this.user.preferences.analyticsConsent) {
|
||||
return this.$t('gpcPlusAnalytics', { url: gpcUrl });
|
||||
}
|
||||
return this.$t('gpcWarning', { url: gpcUrl });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
finalize () {
|
||||
this.setUserPreference('analyticsConsent');
|
||||
localStorage.setItem('analyticsConsent', this.user.preferences.analyticsConsent);
|
||||
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
|
||||
},
|
||||
prefToggled () {
|
||||
@@ -135,7 +192,10 @@ export default {
|
||||
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = newVal;
|
||||
},
|
||||
resetControls () {
|
||||
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
|
||||
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
|
||||
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
|
||||
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -263,11 +263,12 @@ export default {
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
const analyticsConsent = localStorage.getItem('analyticsConsent');
|
||||
if (analyticsConsent !== null
|
||||
&& analyticsConsent !== this.user.preferences.analyticsConsent
|
||||
) {
|
||||
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
|
||||
let analyticsConsent = localStorage.getItem('analyticsConsent');
|
||||
if (analyticsConsent !== null) {
|
||||
analyticsConsent = analyticsConsent === 'true';
|
||||
if (analyticsConsent !== this.user.preferences.analyticsConsent) {
|
||||
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
|
||||
@@ -24,6 +24,8 @@ const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||
|
||||
// Tasks
|
||||
@@ -88,6 +90,9 @@ const router = new VueRouter({
|
||||
path: '/profile/:userId',
|
||||
props: true,
|
||||
},
|
||||
{ name: 'profile', path: '/user/profile' },
|
||||
{ name: 'stats', path: '/user/stats' },
|
||||
{ name: 'achievements', path: '/user/achievements' },
|
||||
{
|
||||
path: '/inventory',
|
||||
component: InventoryContainer,
|
||||
@@ -216,6 +221,28 @@ const router = new VueRouter({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'groupAdmin',
|
||||
path: 'groups',
|
||||
component: GroupAdminPage,
|
||||
meta: {
|
||||
privilegeNeeded: [ // any one of these is enough to give access
|
||||
'groupSupport',
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
name: 'groupAdminGroup',
|
||||
path: ':groupId',
|
||||
component: GroupAdminGroupPage,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'groupsSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'blockers',
|
||||
path: 'blockers',
|
||||
@@ -345,6 +372,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
if (to.params.startingPage !== undefined) {
|
||||
startingPage = to.params.startingPage;
|
||||
}
|
||||
// Check if there's a hash in the URL for stats or achievements
|
||||
if (to.hash === '#stats' || to.hash === '#achievements') {
|
||||
startingPage = to.hash.substring(1);
|
||||
}
|
||||
if (from.name === null) {
|
||||
store.state.postLoadModal = `profile/${to.params.userId}`;
|
||||
return next({ name: 'tasks' });
|
||||
@@ -365,10 +396,18 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
|
||||
if ((to.name === 'stats' || to.name === 'achievements' || to.name === 'profile') && from.name !== null) {
|
||||
const userId = store.state.user.data._id;
|
||||
let redirectPath = `/profile/${userId}`;
|
||||
if (to.name === 'stats') {
|
||||
redirectPath += '#stats';
|
||||
} else if (to.name === 'achievements') {
|
||||
redirectPath += '#achievements';
|
||||
}
|
||||
router.app.$emit('habitica:show-profile', {
|
||||
userId,
|
||||
startingPage: to.name,
|
||||
fromPath: from.path,
|
||||
toPath: to.path,
|
||||
toPath: redirectPath,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
31
website/client/src/store/actions/admin.js
Normal file
31
website/client/src/store/actions/admin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function searchUsers (store, payload) {
|
||||
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getUserHistory (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getSubscriptionPaymentDetails (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/subscription-payment-details`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getGroup (store, payload) {
|
||||
const url = `/api/v4/admin/groups/${payload.groupId}`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function updateGroup (store, payload) {
|
||||
const url = `/api/v4/admin/groups/${payload.groupId || payload.group._id}`;
|
||||
const response = await axios.put(url, payload.group);
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export async function searchUsers (store, payload) {
|
||||
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function getUserHistory (store, payload) {
|
||||
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
@@ -38,3 +38,9 @@ export async function getHeroGroupPlans (store, payload) {
|
||||
const response = await axios.get(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
export async function deleteHero (store, payload) {
|
||||
const url = `/api/v4/members/${payload.uuid}?deleteAccount=${payload.deleteHabiticaAccount}&deleteAmplitude=${payload.deleteAmplitudeData}`;
|
||||
const response = await axios.delete(url);
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
|
||||
|
||||
import * as adminPanel from './adminPanel';
|
||||
import * as admin from './admin';
|
||||
import * as common from './common';
|
||||
import * as user from './user';
|
||||
import * as tasks from './tasks';
|
||||
@@ -26,7 +26,7 @@ import * as blockers from './blockers';
|
||||
// Example: fetch in user.js -> 'user:fetch'
|
||||
|
||||
const actions = flattenAndNamespace({
|
||||
adminPanel,
|
||||
admin,
|
||||
common,
|
||||
user,
|
||||
tasks,
|
||||
|
||||
@@ -12,12 +12,12 @@ describe('renderWithMentions', () => {
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
test('highlights displayname', () => {
|
||||
test('does not highlight displayname to prevent impersonation', () => {
|
||||
const text = 'hello @displayedUser with text after';
|
||||
|
||||
const result = renderMarkdown(text, user('user', 'displayedUser'));
|
||||
|
||||
expect(result).to.contain('<span class="at-text at-highlight">@displayedUser</span>');
|
||||
expect(result).to.contain('<span class="at-text">@displayedUser</span>');
|
||||
expect(result).to.not.contain('<span class="at-text at-highlight">@displayedUser</span>');
|
||||
});
|
||||
|
||||
test('highlights username', () => {
|
||||
@@ -56,7 +56,8 @@ describe('renderWithMentions', () => {
|
||||
|
||||
const result = renderMarkdown(plainText, user('use', 'mentions'));
|
||||
|
||||
expect(result).to.contain('<span class="at-text at-highlight">@mentions</span>');
|
||||
expect(result).to.contain('<span class="at-text">@mentions</span>');
|
||||
expect(result).to.not.contain('<span class="at-text at-highlight">@mentions</span>');
|
||||
expect(result).to.contain('<span class="at-text at-highlight">@use</span>');
|
||||
expect(result).to.contain('<span class="at-text">@mail</span>');
|
||||
expect(result).to.not.contain('<span class="at-text at-highlight">@mentions</span>.com');
|
||||
|
||||
@@ -36,7 +36,7 @@ const envVars = [
|
||||
'TIME_TRAVEL_ENABLED',
|
||||
'DEBUG_ENABLED',
|
||||
'CONTENT_SWITCHOVER_TIME_OFFSET',
|
||||
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'
|
||||
'PLAY_CONSOLE_ORDERS_BASE_URL',
|
||||
];
|
||||
|
||||
const envObject = {};
|
||||
|
||||
@@ -2215,9 +2215,9 @@
|
||||
"armorSpecialWinter2021RogueText": "Efeu-Grünes Gewand",
|
||||
"weaponSpecialWinter2021HealerNotes": "Dirigiere deine Kämpfe mit unvorhersehbarem Schwung, wie ein Schneegestöber! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021HealerText": "Flocken Flanken Rute",
|
||||
"weaponSpecialWinter2021MageNotes": "Diese mächtige Waffe ist nicht nur eine Phase! Konzentriere deine Kräfte, fokussiere den Verlauf eines Monates und studiere den Lauf von Zeit und Raum. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021MageNotes": "Diese mächtige Waffe ist definitiv mehr als nur eine Phase! Konzentriere deine Kräfte, fokussiere auf den Verlauf des Monates und studiere den Lauf von Zeit und Raum. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021MageText": "Magischer Mond-Phaser",
|
||||
"weaponSpecialWinter2021WarriorNotes": "Hiermit kannst Du die größten Frische an Land ziehen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021WarriorNotes": "Hiermit kannst Du die größten Fische an Land ziehen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021WarriorText": "Mächtige Angelrute",
|
||||
"weaponSpecialWinter2021RogueNotes": "Tarnung und Waffe in einem, die giftigen Früchte der Stechpalme helfen dir mit den schwierigsten Aufgaben umzugehen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
|
||||
"weaponSpecialWinter2021RogueText": "Ilex-Beeren Morgenstern",
|
||||
@@ -2861,7 +2861,7 @@
|
||||
"weaponSpecialSummer2024MageText": "Seeanemonen Zauberstab",
|
||||
"weaponSpecialSummer2024MageNotes": "Diese grandiosen Tentakel können Magie gleichzeitig abhalten, ablenken und steuern. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
|
||||
"weaponSpecialSummer2024HealerNotes": "Du wirst erstaunt sein zu entdecken, wie hart die Muschel am Ende des Stabs ist. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
|
||||
"weaponArmoireCorsairsBladeNotes": "Ob du sie zum Plündern oder zum Schutz trägst - du kannst froh sein, daß du diese grimmige Klinge mit dir zur See gebracht hast. Achte nur darauf, daß sie sicher verstaut ist, wenn nicht in Benutzung. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Korsaren Set (Gegenstand 3 von 3)",
|
||||
"weaponArmoireCorsairsBladeNotes": "Ob du sie zum Plündern oder zum Schutz trägst - du kannst froh sein, daß du diese scharfe Klinge mit dir zur See gebracht hast. Achte nur darauf, dass sie sicher verstaut ist, wenn nicht in Benutzung. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Piraten Set (Gegenstand 3 von 3).",
|
||||
"weaponArmoireCorsairsBladeText": "Korsarenklinge",
|
||||
"armorSpecialFall2023MageText": "Scharlachrote Hexenmeister Robe",
|
||||
"armorSpecialFall2023MageNotes": "Mit scharlachrotem Garn und goldenen Akzenten ist dieses Outfit ein Wunder für die Sinne. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2023 Herbstausrüstung.",
|
||||
@@ -2891,7 +2891,7 @@
|
||||
"weaponMystery202408Text": "Arkanes Aegis",
|
||||
"weaponMystery202408Notes": "Ein magisches Bläschen-Schild, das dich vor gegnerischen Zaubersprüchen schützt, oder dir hilft, im Wasser oder in der Luft zu schweben. Gewährt keinen Attributbonus. August 2024 Abonnentengegenstand.",
|
||||
"weaponArmoireDragonKnightsLanceText": "Drachenritter Lanze",
|
||||
"weaponArmoireDragonKnightsLanceNotes": "Diese rot-silberne Lanze hat so manchen Gegner aus dem Sattel seines Reittiers gehoben. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Drachenritter Set (Gegenstand 3 von 3)",
|
||||
"weaponArmoireDragonKnightsLanceNotes": "Diese rot-silberne Lanze hat so manchen Gegner aus dem Sattel seines Reittiers gehoben. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Drachenritter Set (Gegenstand 3 von 3).",
|
||||
"armorSpecialSummer2024WarriorText": "Walhai Schwanzflosse",
|
||||
"armorSpecialSummer2024WarriorNotes": "Nachdem du dich in einen echten Walhai Krieger verwandelt hast, schwimm kühn auf deine Aufgaben zu! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
|
||||
"armorSpecialSummer2024MageText": "Seeanemonen Flosse",
|
||||
@@ -2925,14 +2925,14 @@
|
||||
"armorSpecialFall2024HealerNotes": "Sei eins mit der Galaxis und hypnotisiere Zuschauer mit dieser Rüstung. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2024 Herbstausrüstung.",
|
||||
"armorSpecialFall2024MageText": "Unterwelt Hexer Rüstung",
|
||||
"armorSpecialFall2024MageNotes": "Sei eins mit der Unterwelt und umarme die Macht der Magier, die vor dir diese Rüstung trugen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2024 Herbstausrüstung.",
|
||||
"weaponArmoireFunnyFoolBatonNotes": "Du kannst mit einem Schwung Deines Stabes eine Pointe vortragen, die Aufmerksamkeit erregen oder Beifall ernten. Erhöht Ausdauer und Stärke jeweils um <%= attrs %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 3 von 3)",
|
||||
"weaponArmoireFunnyFoolBatonNotes": "Du kannst mit einem Schwung deines Stabes eine Pointe vortragen, Aufmerksamkeit erregen oder Beifall ernten. Erhöht Ausdauer und Stärke jeweils um <%= attrs %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 3 von 3).",
|
||||
"armorArmoireTeaGownText": "Teekränzchen Kleid",
|
||||
"armorArmoireTeaGownNotes": "Du bist zäh, kreativ, brilliant und so modisch! Erhöht Stärke und Intelligenz um jeweils <%= attrs %>. Verzauberter Schrank: Teekränzchen Set (Gegenstand 1 von 3).",
|
||||
"armorMystery202401Text": "Verschneite Zauberer Roben",
|
||||
"armorMystery202401Notes": "Diese Roben erscheinen filigran wie Schneeflocken, aber werden dich reichlich warm halten, während du deine winterliche Magie wirkst. Gewährt keinen Attributbonus. Januar 2024 Abonnentengegenstand.",
|
||||
"armorMystery202406Notes": "Suche deine Feinde heim mit Stil und Flair! Gewährt keinen Attributbonus. Juni 2024 Abonnentengegenstand.",
|
||||
"armorMystery202407Text": "Liebenswerter Axolotl Anzug",
|
||||
"weaponArmoireSpookyCandyBucketNotes": "Mit einem epischen Kostüm wie diesem wirst du derart viel Süßigkeiten bekommen! Gut, dass du diesen bodenlosen Eimer hast, um das alles aufzunehmen. Versuch, nicht zu naschen, bevor du nach Hause kommst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Süßes oder Saures Set (Gegenstand 2 von 2)",
|
||||
"weaponArmoireSpookyCandyBucketNotes": "Mit einem epischen Kostüm wie diesem wirst du so viele Süßigkeiten bekommen! Gut, dass du diesen Eimer ohne Boden hast, um das alles aufzunehmen. Versuch, nicht zu naschen, bevor du nach Hause kommst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Süßes oder Saures Set (Gegenstand 2 von 2).",
|
||||
"weaponArmoireSpookyCandyBucketText": "Grusliger Süßigkeiten Eimer",
|
||||
"armorMystery202407Notes": "Gleite durch Seen und Kanäle mit deinem hin und her fegenden rosa Schwanz! Gewährt keinen Attributbonus. Juli 2024 Abonnentengegenstand.",
|
||||
"armorArmoireJewelersApronText": "Juweliers-Schürze",
|
||||
@@ -2957,7 +2957,7 @@
|
||||
"armorArmoireKarateGiText": "Karategi",
|
||||
"armorArmoireKarateGiNotes": "Diese leichte Karate-Uniform ist perfekt für Training oder Wettbewerbe. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Karate-Set (Gegenstand 1 von 10).",
|
||||
"weaponArmoireStormKnightAxeText": "Axt des Sturmritters",
|
||||
"weaponArmoireStormKnightAxeNotes": "Sammle deine Wut und schlage wie ein Donnerschlag zu! Erhöht Stärke um <%= str %>. Verzauberter Schrank: Sturmritter-Set (Gegenstand 3 von 3)",
|
||||
"weaponArmoireStormKnightAxeNotes": "Sammle deine Wut und schlage wie ein Donnerschlag zu! Erhöht Stärke um <%= str %>. Verzauberter Schrank: Sturmritter-Set (Gegenstand 3 von 3).",
|
||||
"armorArmoireDiagonalRainbowShirtNotes": "Ein Klecks Farbe mit einem Schuss Stil. Sei fröhlich! Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %> . Verzauberter Schrank: Regenbogen-Set (Gegenstand 2 von 2).",
|
||||
"armorArmoireAdmiralsUniformText": "Admirals-Uniform",
|
||||
"armorArmoireAdmiralsUniformNotes": "Wir salutieren dir! Diese Marineuniform signalisiert, dass du bereit bist, das Kommando sowohl von deinen Aufgaben als auch von einem Schiff zu übernehmen. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Admirals-Set (Gegenstand 2 von 2).",
|
||||
@@ -3315,7 +3315,7 @@
|
||||
"headMystery202504Notes": "Trage diese mysteriöse Visage, um unentdeckt unter den oskursten Fabelwesen der Welt zu verweilen. Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
|
||||
"headArmoireSillierBlueTophatNotes": "Etwas Klasse, etwas Raffinesse. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 2 von 2).",
|
||||
"weaponArmoireGildedKnightsSpearText": "Vergoldeter Ritter Speer",
|
||||
"weaponArmoireGildedKnightsSpearNotes": "Mit dieser Waffe kannst du sicherstellen, dass jeder immer seine Schulden bezahlt. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 3 von 3)",
|
||||
"weaponArmoireGildedKnightsSpearNotes": "Mit dieser Waffe kannst du sicherstellen, dass jeder immer seine Schulden bezahlt. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 3 von 3).",
|
||||
"armorArmoireGildedKnightsPlateNotes": "In dieser Rüstung bist du fast unbesiegbar. Deine Feinde werden dich sicher dröhnen hören! Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 2 von 3)",
|
||||
"armorArmoireGildedKnightsPlateText": "Vergoldeter Ritter Rüstung",
|
||||
"headArmoireGildedKnightsHelmText": "Vergoldeter Ritter Helm",
|
||||
@@ -3330,7 +3330,7 @@
|
||||
"weaponSpecialSummer2025RogueNotes": "Dieser Tentakel wird deine Ziele eng umklammern, so dass du den Schwung nicht verlierst, wenn du deine Aufgaben erledigst. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Sommerausrüstung 2025.",
|
||||
"weaponSpecialSummer2025HealerText": "Ruderschnecken Flügelpaddel",
|
||||
"weaponSpecialSummer2025HealerNotes": "Beschreibe rudernd einen Doppelkreis, während du dich vorwärts bewegst, und bei deinen Aufgaben tolle Fortschritte machst. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Sommerausrüstung 2025.",
|
||||
"weaponArmoireBeekeepersSmokerNotes": "Benutze dies, um deine Bienen zu beruhigen, damit du den Honig besser holen kannst. Die Bienen wird es nicht stören. Ehrlich, wir alle könnten von Zeit zu Zeit ein paar extra Minuten Ruhe gebrauchen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Imker Set (Gegenstand 3 von 4)",
|
||||
"weaponArmoireBeekeepersSmokerNotes": "Benutze dies, um deine Bienen zu beruhigen, damit du den Honig besser holen kannst. Die Bienen wird es nicht stören. Um ehrlich zu sein, wir alle könnten von Zeit zu Zeit ein paar extra Minuten Ruhe gebrauchen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Imker Set (Gegenstand 3 von 4).",
|
||||
"weaponArmoireBeekeepersSmokerText": "Smoker",
|
||||
"armorSpecialSummer2025WarriorText": "Kammmuschel Rüstung",
|
||||
"armorSpecialSummer2025WarriorNotes": "Diese Rüstung macht dich nicht nur robust, sondern auch schnell. Kampf oder Flucht - es ist deine Wahl! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Sommerausrüstung 2025.",
|
||||
@@ -3344,7 +3344,7 @@
|
||||
"armorArmoireBeekeepersSuitNotes": "Schütze dich, während du nach deinen fleißigen Hummeln schaust. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Imker Set (Gegenstand 2 von 4)",
|
||||
"headSpecialSummer2025RogueNotes": "Deine Sehkraft wird sich verbessern sobald Du diese Maske aufsetzt. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommer 2025 Ausrüstung.",
|
||||
"weaponSpecialSummer2025MageText": "Zweigkoralle",
|
||||
"weaponSpecialSummer2025MageNotes": "Verzweige dich mit deinen Talenten und Fähigkeiten, um eine Reihe von verschiedenen Aufgaben anzugreifen. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommerausrüstung 2025",
|
||||
"weaponSpecialSummer2025MageNotes": "Verzweige dich mit deinen Talenten und Fähigkeiten, um eine Reihe von verschiedenen Aufgaben anzugreifen. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommerausrüstung 2025.",
|
||||
"headSpecialSummer2025WarriorText": "Kammmuschelhelm",
|
||||
"headSpecialSummer2025WarriorNotes": "Undurchdringlich und perfekt zugespitzt wird dich dieser Helm sogar vor Seesternen beschützen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Sommerausrüstung 2025.",
|
||||
"headSpecialSummer2025RogueText": "Tintenfischmaske",
|
||||
@@ -3386,5 +3386,58 @@
|
||||
"weaponMystery202508Text": "Funkelnde Blutrote klinge",
|
||||
"weaponMystery202508Notes": "Diese wirbelnde Klinge wird jedes Monster oder rote Tagesaufgabe, die deinen Weg kreuzen, in Schrecken versetzen! Gewährt keinen Attributbonus. August 2025 Abonnentengegenstand.",
|
||||
"weaponMystery202511Text": "Frostschwert",
|
||||
"weaponMystery202511Notes": "Der eisige Glanz dieses Schwertes wird selbst mit dunkelroten Aufgaben kurzen Prozess machen. Gewährt keinen Attributbonus. Novermber 2025 Abonnentengegenstand."
|
||||
"weaponMystery202511Notes": "Der eisige Glanz dieses Schwertes wird selbst mit dunkelroten Aufgaben kurzen Prozess machen. Gewährt keinen Attributbonus. Novermber 2025 Abonnentengegenstand.",
|
||||
"weaponArmoireBlacksmithsHammerText": "Schmiedehammer",
|
||||
"weaponArmoireBlacksmithsHammerNotes": "Dieser Hammer ist für Metallarbeiten, er ist aber auch ideal geeignet inmitten von rotglühenden Kohlen und rotglühenden Tagesaufgaben. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Schmiedeset (Gegenstand 3 von 3).",
|
||||
"armorSpecialFall2025WarriorText": "Sasquatch Rüstung",
|
||||
"armorSpecialFall2025WarriorNotes": "Weder deine großen Füße, noch dein großer Körper wird zu groß sein um in diese saisonale Rüstung zu passen. Erhöht Ausdauer um <%= con %>. Limitierte Herbstausrüstung 2025.",
|
||||
"armorSpecialFall2025RogueText": "Skelett Rüstung",
|
||||
"armorSpecialFall2025HealerText": "Kobold Rüstung",
|
||||
"armorSpecialFall2025HealerNotes": "Diese saisonale Rüstung lässt dich mit den dunklen Wäldern verschmelzen um einen strategischen Rückzug anzutreten. Erhöht Ausdauer um <%= con %>. Limitierte Herbstausrüstung 2025.",
|
||||
"armorSpecialFall2025MageText": "Maskierter Geist Rüstung",
|
||||
"armorSpecialFall2025MageNotes": "Diese saisonale Rüstung wird körperlos sobald du sie anziehst. Erhöht Intelligenz um <%= int %>. Limitierte Herbstausrüstung 2025.",
|
||||
"armorMystery202509Text": "Robe des Windgepeitschten Wanderers",
|
||||
"armorMystery202509Notes": "Helle Seide schützt dich vor dem Wetter - sowohl heiß als auch kalt. Gewährt keinen Attributbonus. September 2025 Abonnentengegenstand.",
|
||||
"armorArmoireRedWaistcoatText": "Rote Weste",
|
||||
"armorArmoireSoftOrangeSuitText": "Weicher Orangener Anzug",
|
||||
"armorArmoireBlacksmithsApronText": "Schmiedeschürze",
|
||||
"armorArmoireBlacksmithsApronNotes": "Diese Schürze fühlt sich nicht so schwer an, wie sie aussieht, sobald du sie trägst. Sie wird dich vor Funken schützen und dir gleichzeitig ermöglichen, dich frei zu bewegen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Schmied Set (Gegenstand 2 von 3).",
|
||||
"armorArmoireBlackPartyDressText": "Schwarzes Party Kleid",
|
||||
"armorArmoireBlackPartyDressNotes": "Du bist stark, schlau, tüchtig und so modisch! Erhöht Stärke, Intelligenz und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Schwarzes Haarschleifen-Set (Gegenstand 2 von 2).",
|
||||
"headSpecialFall2025WarriorText": "Sasquatch Maske",
|
||||
"headSpecialFall2025WarriorNotes": "Rund und haarig - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Stärke um <%= str %>. Limitierte Herbstausrüstung 2025.",
|
||||
"headSpecialFall2025RogueText": "Skelett Maske",
|
||||
"headSpecialFall2025RogueNotes": "Blass und vermummt - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Wahrnehmung um <%= per %>. Limitierte Herbstausrüstung 2025.",
|
||||
"headSpecialFall2025HealerText": "Kobold Maske",
|
||||
"headSpecialFall2025MageText": "Maskierter Geist Maske",
|
||||
"headSpecialFall2025MageNotes": "Ätherisch und glühend - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Wahrnehmung um <%= per %>. Limitierte Herbstausrüstung 2025.",
|
||||
"armorArmoireRedWaistcoatNotes": "Sieh elegant und umwerfend aus, während du deine Aufgaben bewältigst. In der Westentasche ist etwas geheimes versteckt — was denkst du, könnte es sein? Erhöht Ausdauer und Stärke um jeweils <%= attrs %>. Verzauberter Schrank: Rote Weste Set (Gegenstand 2 von 2)",
|
||||
"armorArmoireSoftOrangeSuitNotes": "Orange ist eine lebhafte Farbe. Zieh dies an, wenn du zu Bett gehst und in allen Abenteuern, denen du in deinen Träumen begegnest, wirst du sicher Erfolg haben. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Oranges Loungewear-Set (Gegenstand 2 von 3).",
|
||||
"headSpecialFall2025HealerNotes": "Markant und gehörnt - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Intelligenz um <%= int %>. Limitierte Herbstausrüstung 2025.",
|
||||
"headArmoireRedNewsieHatText": "Rote Zeitungsjungenmütze",
|
||||
"headArmoireRedNewsieHatNotes": "Extra! Extra! Lesen Sie alles darüber: Diese Mütze ist bequem, modisch und praktisch. Erhöht die Wahrnehmung und Intelligenz um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Rotes Weste-Set (Item 1 von 2)",
|
||||
"headArmoireFloppyOrangeHatText": "Orangener Schlapphut",
|
||||
"headArmoireFloppyOrangeHatNotes": "In diesen simplen Hut wurden zahlreiche Zauber eingearbeitet, die ihm eine auffällige orange Farbe verleihen. Erhöht alle Werte um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Orangenes Loungewear-Set (Item 1 von 3).",
|
||||
"headArmoireBlackHairbowText": "Schwarze Haarschleife",
|
||||
"headArmoireBlackHairbowNotes": "Werde stark, klug und herzhaft, während du diese wunderschöne schwarze Haarschleife trägst! Erhöht Stärke, Intelligenz und Konstitution um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Schwarzes Haarschleifen-Set (Item 1 von 2).",
|
||||
"headArmoireBlacksmithsGogglesText": "Schmiedebrille",
|
||||
"shieldSpecialFall2025RogueNotes": "Eine mächtige Waffe, mit der Sie Ihre To-Do's um die Hälfte reduzieren können. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Herbst 2025 Ausrüstung.",
|
||||
"shieldSpecialFall2025HealerText": "Koboldschild",
|
||||
"shieldMystery202508Text": "Brillante Cyan-Klinge",
|
||||
"shieldMystery202508Notes": "Wenn Sie schon eine rotierende Klinge cool fanden, probieren Sie doch mal zwei! Bietet keinen Vorteil. August 2025 Abonnentengegenstand.",
|
||||
"shieldMystery202511Text": "Frostschild",
|
||||
"shieldMystery202511Notes": "Dieser robuste Schild aus eisigem Gestein schützt dich vor schlechten Gewohnheiten, ohne deine Hände zu vereisen. Verleiht keinen Vorteil. November 2025 Abonnentengegenstand.",
|
||||
"shieldArmoireSoftOrangePillowText": "Weiches orangenes Kissen",
|
||||
"backMystery202510Text": "Gleitende Ghulflügel",
|
||||
"backMystery202510Notes": "Fliege mit diesen riesigen Flügeln lautlos durch den heimgesuchten Himmel. Verleiht keinen Vorteil. Oktober 2025 Abonnentengegenstand.",
|
||||
"bodyMystery202509Notes": "Dieser Schal schützt dein Gesicht vor Wind und sieht auch noch verdammt cool aus. Bietet keinen Vorteil. September 2025 Abonnentengegenstand.",
|
||||
"eyewearMystery202510Text": "Gleitende Ghul-Augen",
|
||||
"eyewearMystery202510Notes": "Diese gruseligen Augen leuchten wie der Erntemond. Verleiht keinen Vorteil. Oktober 2025 Abonnentengegenstand.",
|
||||
"headArmoireBlacksmithsGogglesNotes": "Bei der Arbeit in einer Schmiede benötigen Sie einen bruchsicheren und hitzebeständigen Augenschutz. Erhöht die Wahrnehmung um <%= per %>. Verzauberter Schrank: Schmiedeset (Item 1 von 3).",
|
||||
"shieldSpecialFall2025WarriorText": "Sasquatch Schild",
|
||||
"shieldSpecialFall2025WarriorNotes": "Verschaffe dir etwas mehr Zeit zum Nachdenken und Planen, indem du dich vor deinen nächsten Tagesaufgaben abschirmst. Erhöht die Konstitution um <%= con %>. Limitierte Auflage Herbst 2025 Ausrüstung.",
|
||||
"shieldSpecialFall2025HealerNotes": "Verschaffe dir etwas mehr Zeit, um Vorräte zu sammeln, indem du dich vor deinen Aufgaben abschirmst. Erhöht die Konstitution um <%= con %>. Limitierte Ausgabe Herbst 2025 Ausrüstung.",
|
||||
"shieldArmoireSoftOrangePillowNotes": "Der vorbereitete Krieger packt für jede Expedition ein Kissen ein. Mach dich bereit, neue Verpflichtungen zu übernehmen ... sogar während du ein Nickerchen machst. Erhöht Intelligenz und Wahrnehmung um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Orangenes Loungewear-Set (Gegenstand 3 von 3).",
|
||||
"bodyMystery202509Text": "Schal des windgepeitschten Wanderers",
|
||||
"armorSpecialFall2025RogueNotes": "Ein hartes und schmales Ziel in dieser saisonalen Rüstung ist am schwersten zu treffen. Erhöht die Wahrnehmung um <%= per %>. Limitierte Ausgabe Herbst 2025 Ausrüstung."
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stable": "Haus- und Reittiere",
|
||||
"stable": "Haustiere und Reittiere",
|
||||
"pets": "Haustiere",
|
||||
"activePet": "Aktives Haustier",
|
||||
"noActivePet": "Kein aktives Haustier",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"settings": "Einstellungen",
|
||||
"language": "Sprache",
|
||||
"americanEnglishGovern": "Im Fall von Bedeutungsunterschieden gilt die englische Version.",
|
||||
"helpWithTranslation": "Hast du Interesse, bei der Übersetzung von Habitica helfen? Toll! Dann besuche doch die <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">Aspiring Linguists Guild</a>!",
|
||||
"helpWithTranslation": "Hast du Interesse, bei der Übersetzung von Habitica helfen? Toll! Dann besuche doch die <a href=\"https://translate.habitica.com\"> Habitica's Weblate Seite</a>!",
|
||||
"stickyHeader": "Kopfzeile anheften",
|
||||
"newTaskEdit": "Neue Aufgaben im Bearbeiten-Modus öffnen",
|
||||
"reverseChatOrder": "Zeige die Chat-Nachrichten in umgekehrter Reihenfolge",
|
||||
|
||||
@@ -3,5 +3,9 @@
|
||||
"siteBlockers": "Site Blockers",
|
||||
"newsroom": "Newsroom",
|
||||
"adminBlockerTypeDescription": "<b>IP-Address</b> - Block access for a specific IP-Address\n\nClient - Block access for a client based on the \"x-client\" header.\n\nE-Mail - Blocks e-mails from being used for signup.",
|
||||
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed."
|
||||
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed.",
|
||||
"groupAdmin": "Group Admin",
|
||||
"groupSupportDescription": "Manage groups and their members. You can search for groups by ID, or load your own group by leaving the field blank.",
|
||||
"groupData": "Group Data",
|
||||
"groupPlanSubscription": "Group Plan Subscription"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"awardWinners": "Award Winner",
|
||||
"doYouWantedToDeleteChallenge": "Do you want to delete this Challenge?",
|
||||
"deleteChallenge": "Delete Challenge",
|
||||
"deleteChallengeRefundDescription": "If you delete this Challenge, you will be refunded the Gem prize and the Challenge tasks will remain on the participants' task boards.",
|
||||
"challengeNamePlaceholder": "What is your Challenge name?",
|
||||
"challengeSummary": "Summary",
|
||||
"challengeSummaryPlaceholder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"notEnoughGems": "Not enough Gems",
|
||||
"alreadyHave": "Whoops! You already have this item. No need to buy it again!",
|
||||
"delete": "Delete",
|
||||
"gem": "Gem",
|
||||
"gems": "Gems",
|
||||
"needMoreGems": "Need More Gems?",
|
||||
"needMoreGemsInfo": "Purchase Gems now, or become a subscriber to buy Gems with Gold, get monthly mystery items, enjoy increased drop caps and more!",
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"needsTextPlaceholder": "Type your message here.",
|
||||
"messageCopiedToClipboard": "Message copied to clipboard.",
|
||||
"leaderOnlyChallenges": "Only group leader can create challenges",
|
||||
"sendGift": "Send a Gift",
|
||||
"sendGift": "Send Gift",
|
||||
"selectGift": "Select Gift",
|
||||
"selectSubscription": "Select Subscription",
|
||||
"sendGiftToWhom": "Who would you like to send a gift to?",
|
||||
|
||||
@@ -271,5 +271,7 @@
|
||||
"performanceAnalytics": "Performance and Analytics",
|
||||
"usedForSupport": "These are used to improve the user experience, performance, and services of our website and apps. This data is used by our support team when handling requests and bug reports.",
|
||||
"savePreferences": "Save Preferences",
|
||||
"habiticaPrivacyPolicy": "Habitica's Privacy Policy"
|
||||
"habiticaPrivacyPolicy": "Habitica's Privacy Policy",
|
||||
"gpcWarning": "<a href='<%= url %>' target='_blank'>GPC</a> is on. Turning on tracking below will override this and send data to our analytics partners.",
|
||||
"gpcPlusAnalytics": "<a href='<%= url %>' target='_blank'>GPC</a> is on. You have opted in to tracking and sending data to our analytics partners."
|
||||
}
|
||||
|
||||
@@ -901,13 +901,25 @@
|
||||
"backgrounds0420205": "SET 131: Released April 2025",
|
||||
"backgroundGardenWithFlowerBedsText": "Garden with Flower Beds",
|
||||
"backgroundGardenWithFlowerBedsNotes": "Enjoy the blooms of spring in a Garden with Flower Beds.",
|
||||
"backgrounds062025": "第 133 组:由2025 年 6 月发布",
|
||||
"backgroundSummerSeashoreText": "夏日海滨",
|
||||
"backgroundSummerSeashoreNotes": "在夏日海滨乘风破浪.",
|
||||
"backgrounds052025": "第 132 组:于2025 年 5 月发布",
|
||||
"backgroundTrailThroughAForestText": "穿越森林的小径",
|
||||
"backgroundTrailThroughAForestNotes": "沿着穿过森林的小径漫步。",
|
||||
"backgrounds062025": "SET 133: Released June 2025",
|
||||
"backgroundSummerSeashoreText": "Summer Seashore",
|
||||
"backgroundSummerSeashoreNotes": "Catch a wave at a Summer Seashore.",
|
||||
"backgrounds052025": "SET 132: Released May 2025",
|
||||
"backgroundTrailThroughAForestText": "Trail Through a Forest",
|
||||
"backgroundTrailThroughAForestNotes": "Wander down a Trail Through a Forest.",
|
||||
"backgrounds072025": "SET 134: Released July 2025",
|
||||
"backgroundSirensLairText": "Siren's Lair",
|
||||
"backgroundSirensLairNotes": "Dare to dive into a Siren’s Lair."
|
||||
"backgroundSirensLairNotes": "Dare to dive into a Siren’s Lair.",
|
||||
"backgrounds082025": "SET 135: Released August 2025",
|
||||
"backgroundSunnyStreetWithShopsText": "Sunny Street with Shops",
|
||||
"backgroundSunnyStreetWithShopsNotes": "Enjoy the sights and sounds of a Sunny Street with Shops.",
|
||||
"backgrounds092025": "SET 136: Released September 2025",
|
||||
"backgroundAutumnSwampText": "Autumn Swamp",
|
||||
"backgroundAutumnSwampNotes": "Take in the haunting vibes of an Autumn Swamp.",
|
||||
"backgrounds102025": "SET 137: Released October 2025",
|
||||
"backgroundInsideForestWitchsCottageText": "Forest Witch's Cottage",
|
||||
"backgroundInsideForestWitchsCottageNotes": "Weave spells inside a Forest Witch's Cottage.",
|
||||
"backgrounds112025": "SET 138: Released November 2025",
|
||||
"backgroundCastleKeepWithBannersText": "Castle Hall with Banners",
|
||||
"backgroundCastleKeepWithBannersNotes": "Sing tales of heroic deeds in a Castle Hall with Banners."
|
||||
}
|
||||
|
||||
@@ -3386,10 +3386,10 @@
|
||||
"weaponSpecialFall2025WarriorNotes": "Una poderosa arma capaz de trazar una senda segura a través de los peligros del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025RogueNotes": "Una poderosa arma capaz de trazar una senda segura a través de los peligros del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025RogueText": "Espada Esqueleto",
|
||||
"weaponSpecialFall2025MageNotes": "Una poderosa arma capaz de trazar una senda segura a través de los terrores del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025MageNotes": "Una poderosa arma capaz de trazar una senda segura a través de los terrores del Bosque Negro. Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025WarriorText": "Hacha de Bigfoot",
|
||||
"weaponSpecialFall2025HealerText": "Hacha Kobold",
|
||||
"weaponSpecialFall2025HealerNotes": "Una poderosa arma capaz de trazar una senda segura a través de los obstáculos del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025HealerNotes": "Una poderosa arma capaz de trazar una senda segura a través de los obstáculos del Bosque Negro. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada Otoño 2025.",
|
||||
"weaponSpecialFall2025MageText": "Hacha de Fantasma Enmascarado",
|
||||
"weaponMystery202511Text": "Espada Escarcha",
|
||||
"weaponMystery202511Notes": "El halo helado de esta espada te permitirá realizar con rapidez incluso las tareas rojas más oscuras. No otorga ningún beneficio. Artículo de Suscriptor Noviembre 2025.",
|
||||
|
||||
@@ -906,5 +906,20 @@
|
||||
"backgroundSummerSeashoreNotes": "Cavalca un'onda su una Spiaggia Estiva.",
|
||||
"backgrounds052025": "SET 132: Rilasciato a Maggio 2025",
|
||||
"backgroundTrailThroughAForestText": "Sentiero Attraverso una Foresta",
|
||||
"backgroundTrailThroughAForestNotes": "Passeggia lungo un Sentiero Attraverso una Foresta."
|
||||
"backgroundTrailThroughAForestNotes": "Passeggia lungo un Sentiero Attraverso una Foresta.",
|
||||
"backgrounds072025": "SET 134: Rilasciato Luglio 2025",
|
||||
"backgroundSirensLairText": "Tana della Sirena",
|
||||
"backgroundSirensLairNotes": "Abbi il coraggio di entrare nella Tana della Sirena.",
|
||||
"backgrounds082025": "SET 135: Rilasciato Agosto 2025",
|
||||
"backgroundSunnyStreetWithShopsText": "Strada Soleggiata con Negozi",
|
||||
"backgroundSunnyStreetWithShopsNotes": "Lasciati incantare dall’atmosfera di una strada soleggiata con negozi.",
|
||||
"backgroundAutumnSwampText": "Palude Autunnale",
|
||||
"backgroundAutumnSwampNotes": "Immergiti nelle inquietanti atmosfere di una palude d’autunno.",
|
||||
"backgrounds102025": "SET 137: Rilasciato Ottobre 2025",
|
||||
"backgroundInsideForestWitchsCottageNotes": "Tessi incantesimi nella casetta della Strega della Foresta.",
|
||||
"backgrounds112025": "SET 138: Rilasciato Novembre 2025",
|
||||
"backgroundCastleKeepWithBannersText": "Sala del castello con stendardi",
|
||||
"backgrounds092025": "SET 136: Rilasciato Settembre 2025",
|
||||
"backgroundInsideForestWitchsCottageText": "Casetta della Strega della Foresta",
|
||||
"backgroundCastleKeepWithBannersNotes": "Canta storie di gesta eroiche nella sala del castello con gli stendardi."
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"subGemName": "Gemme abbonato",
|
||||
"maxBuyGems": "Hai comprato tutte le Gemme a disposizione per questo mese. Altre saranno disponibili entro i primi tre giorni del mese prossimo. Grazie per esserti abbonato!",
|
||||
"timeTravelers": "Viaggiatori del Tempo",
|
||||
"timeTravelersPopoverNoSubMobile": "Pare che tu abbia bisogno di una Clessidra Mistica per aprire il portale temporale ed evocare i Misteriosi Viaggiatori del Tempo.",
|
||||
"timeTravelersPopoverNoSubMobile": "Gli abbonati ricevono una rara Clessidra Mistica ogni mese da utilizzare nel negozio dei Viaggiatori del Tempo.",
|
||||
"timeTravelersPopover": "La tua Clessidra Mistica ha aperto il nostro portale temporale! Scegli cosa vorresti recuperare dal passato o dal futuro.",
|
||||
"mysterySetNotFound": "Completo Mistery non trovato o già posseduto.",
|
||||
"mysteryItemIsEmpty": "Gli oggetti Mistery sono finiti",
|
||||
@@ -120,14 +120,14 @@
|
||||
"gemBenefit2": "Sfondi per immergere il tuo avatar nel mondi di Habitica!",
|
||||
"gemBenefit3": "Entusiasmanti Missioni che hanno come ricompensa uova di animali.",
|
||||
"gemBenefit4": "Resetta le statistiche del tuo avatar e cambia la sua classe.",
|
||||
"subscriptionBenefit1": "Alexander il Mercante ti venderà ora delle Gemme nel Mercato al prezzo di 20 Oro l'una!",
|
||||
"subscriptionBenefit3": "Scopri ancora più oggetti in Habitica con un bottino giornaliero raddoppiato.",
|
||||
"subscriptionBenefit4": "Ogni mese un costume unico e alla moda per il tuo avatar.",
|
||||
"subscriptionBenefit5": "Riceverai il Lepronte Porpora una volta diventato un nuovo Abbonato.",
|
||||
"subscriptionBenefit6": "Ottieni Clessidre Mistiche per comprare oggetti nel negozio dei Viaggiatori del Tempo!",
|
||||
"subscriptionBenefit1": "Ottieni fino a 50 gemme acquistabili con Oro nel Mercato per comprare Missioni, Personalizzazioni, Animali ed altro!",
|
||||
"subscriptionBenefit3": "Trova il doppio di Uova, Pozioni di schiusa e cibo ogni giorno per accrescere la tua collezioni di Animali!",
|
||||
"subscriptionBenefit4": "Rimani aggiornato con gli ultimi equipaggiamenti esclusivi. Abbonati adesso per ottenere <%= month %>’s <%= currentMysterySetName %>!",
|
||||
"subscriptionBenefit5": "Ottieni l'esclusivo Lepronte Porpora una volta che ti sei abbonato oggi!",
|
||||
"subscriptionBenefit6": "Non perderti neanche un oggetto con 1 Clessidra Mistica al mese da utilizzare nel negozio dei Viaggiatori del Tempo!",
|
||||
"purchaseAll": "Acquista Set",
|
||||
"gemsRemaining": "Gemme rimanenti",
|
||||
"notEnoughGemsToBuy": "Non puoi comprare quella quantità di Gemme",
|
||||
"gemsRemaining": "rimanenti",
|
||||
"notEnoughGemsToBuy": "Non ci sono più gemme disponibili per l'acquisto questo mese. Altre diventeranno disponibili entro i primi 3 giorni di ogni mese.",
|
||||
"subWillBecomeInactive": "Diventerà inattivo",
|
||||
"confirmCancelSub": "Sei sicuro di voler cancellare il tuo abbonamento? Perderai tutti i tuoi benefici.",
|
||||
"mysticHourglassNeededNoSub": "Questo articolo ha bisogno di una Clessidra Mistica. Ne otterrai sottoscrivendo un abbonamento ad Habitica.",
|
||||
@@ -226,5 +226,26 @@
|
||||
"mysterySet202305": "Set Drago del Vespro",
|
||||
"mysterySet202302": "Set del Prestigiatore Striato",
|
||||
"mysterySet202304": "Set Teiera Eccellente",
|
||||
"mysterySet202310": "Set Fantasma della Luce Spettrale"
|
||||
"mysterySet202310": "Set Fantasma della Luce Spettrale",
|
||||
"mysterySet202508": "Set della Lama Brillante",
|
||||
"mysterySet202403": "Set della Leggenda Fortunata",
|
||||
"mysterySet202409": "Set del Mago dell'Eliotropio",
|
||||
"mysterySet202407": "Set dell'amabile Axolotl",
|
||||
"mysterySet202509": "Set del Viandante spazzato dal vento",
|
||||
"mysterySet202511": "Set del Guerriero del Gelo",
|
||||
"mysterySet202408": "Set dell'Egida Arcana",
|
||||
"mysterySet202504": "Set dello Yeti sfuggente",
|
||||
"mysterySet202402": "Set del Paradiso Rosa",
|
||||
"mysterySet202412": "Set del Coniglietto del Bastoncino di Zucchero",
|
||||
"subscribeTo": "Abbonati a",
|
||||
"mysterySet202503": "Set Furia di Giada",
|
||||
"mysterySet202404": "Set del Mago dei Funghi",
|
||||
"mysterySet202405": "Set del Drago Dorato",
|
||||
"mysterySet202501": "Set del Vincolagelo",
|
||||
"mysterySet202502": "Set dell'Arlecchino sincero",
|
||||
"mysterySet202406": "Set del Bucaniere Fantasma",
|
||||
"mysterySet202411": "Set del Combattente peloso",
|
||||
"mysterySet202506": "Set della Luce Solare",
|
||||
"mysterySet202505": "Set della Coda di rondine volante",
|
||||
"mysterySet202507": "Set dello Skater coraggioso"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"webFaqAnswer37": "「衣装を着る」オプションがオンになっているか確認してください。もしアバターが衣装を着ている場合、その装備セットが武装の代わりに表示されます。\n\nモバイル版で衣装を切り替えるには:\n * メニューから「装備」を選択し、「衣装を使用する」トグルを見つけます\n\nウェブ版で衣装を切り替えるには:\n * 所持品から「装備」を選択し、装備ドロワーの衣装タブで「衣装を使用する」トグルを見つけます",
|
||||
"webFaqAnswer38": "新しいHabiticaプレイヤーは基本の戦士クラスの装備しか購入できません。プレイヤーは装備を順番に購入して次のピースをアンロックする必要があります。\n\n多くの装備はクラス固有であり、プレイヤーは現在のクラスに属する装備のみ購入できます。",
|
||||
"webFaqAnswer39": "もし装備を手に入れたい場合は、Habiticaの有料会員になるか、ラッキー宝箱に挑戦するか、Habiticaの大祭のうちの1つで贅沢をすることができます。\n\nHabiticaの有料会員は毎月特別な独占装備セットと、タイムトラベラーショップで過去の装備セットを購入するための神秘的な砂時計を受け取ります。\n\nごほうびのラッキー宝箱には350以上の装備があります!100ゴールドで、特別な装備、ペットを乗騎に育てるための餌、またはレベルアップのための経験値のいずれかを受け取るチャンスがあります!\n\n四季ごとの大祭では、新しいクラスの装備がゴールドで購入でき、以前の大祭セットはジェムで購入できます。",
|
||||
"webFaqAnswer41": "神秘の砂時計はHabiticaの有料会員限定の通貨でタイムトラベラーの店で使用できます。有料会員は神秘の砂時計を登録特典がある月ごとに他の特典とともに受け取れます。\n特別な背景、ペット、クエスト、道具に興味があるならば有料プランのオプションをタイムトラベラーの店でチェックしてみてください。",
|
||||
"webFaqAnswer41": "神秘の砂時計はHabiticaの有料会員限定の通貨で、タイムトラベラーの店で使用できます。有料会員は登録している間、神秘の砂時計と、ほかにもいろいろな特典を毎月受け取れます。タイムトラベラーの店で買える特別な背景、ペット、クエストや装備に興味がある方は、ぜひ有料プランのオプションをチェックしてみてください!",
|
||||
"webFaqAnswer42": "自分自身を奮起させ、タスクを達成するために責任感を高める最良の方法の一つは、パーティーに参加することです! 他のHabiticaプレイヤーと一緒にパーティーを組むことは、クエストに挑戦してペットと装備を手に入れ、仲間のスキルからバフを受け、モチベーションを高める素晴らしい方法です。\n\n責任感を高める別の方法は、チャレンジに参加することです。 チャレンジは特定の目標に関連するタスクを自動的にリストに追加します! これにより、ジェムの賞を目指して競争する要素が追加され、モチベーションが向上するかもしれません。 Habiticaチームによって作成された公式のチャレンジだけでなく、他のプレイヤーによって作成されたチャレンジもあります。",
|
||||
"faqQuestion43": "クエストの進め方は?",
|
||||
"webFaqAnswer43": "クエストを始めるには、まずパーティーに参加する必要があります。パーティーは、単独でクエストに挑戦する冒険としても、他のHabiticaプレーヤーを招待してクエストに迅速に取り組むこともできます!\n\nパーティーから「クエストの開始」ボタンを選択して、インベントリからクエストスクロールを選択します。クエストを進めるために通常どおりタスクを完了してください!ボスクエストに挑戦している場合はモンスターに対してダメージを蓄積し、コレクションクエストに挑戦している場合はアイテムを見つけるチャンスがあります。すべての進捗状況は翌日に適用されます。\n\n十分なダメージを与えたり、すべてのアイテムを集めたりすると、クエストが完了し、報酬がもらえます!",
|
||||
@@ -192,7 +192,7 @@
|
||||
"subscriptionDetail001": "すべての有料会員は神秘の砂時計を毎月のミステリーアイテムと同じく毎月同じ日にもらえます。",
|
||||
"subscriptionDetail45": "もし追加で有料プランをプレゼントされた場合に神秘の砂時計を多くもらえたりジェムの最大値が早く増えたりはしますか?",
|
||||
"subscriptionDetail480": "これらの変更は神秘の砂時計と有料会員のジェムにのみ反映されます。そのほかすべての得点はそのまま残ります。",
|
||||
"subscriptionPara2": "もし上記の回答にない質問があるならば、いつでも <%= mailto %>.に連絡してください",
|
||||
"subscriptionPara2": "もし上記の回答にない質問があるならば、いつでも <%= mailto %>に連絡してください。",
|
||||
"subscriptionBenefitsAdjustments": "有料会員の特典調整",
|
||||
"subscriptionBenefitsFaqTitle": "有料会員の特典調整のよくある質問",
|
||||
"subscriptionPara0": "私たちはHabiticaの有料プランをより多くの神秘の砂時計とジェムで過去最高のものにしました!これらの変更は有料プランの特典をもっとわかりやすくするものです。",
|
||||
@@ -227,5 +227,12 @@
|
||||
"subscriptionDetail24": "有料会員が「タイムトラベラーズショップ」からアイテムを収集できる機会を、年間4回以上に増やしたいと考えました。",
|
||||
"subscriptionHeading3": "リリース当日特典",
|
||||
"subscriptionPara1": "新しいスケジュールへの移行をスムーズにするため、既存の有料会員にはリリース当日に追加の特典が用意されます。この変更に伴い、引き続きサポートしてくださる皆様に心から感謝いたします!",
|
||||
"subscriptionDetail4400": "現在、毎月ジェムを<%= initialNumber %>個アンロックしている場合、ジェムの上限は<%= roundedNumber %>個に調整されます。"
|
||||
"subscriptionDetail4400": "現在、毎月ジェムを<%= initialNumber %>個アンロックしている場合、ジェムの上限は<%= roundedNumber %>個に調整されます。",
|
||||
"subscriptionDetail48": "ミステリー装備セットなど、他の有料プランの特典には変更ありますか?",
|
||||
"subscriptionDetail33": "このご褒美は、11月19日以前に有料プランを始めたアカウントしか受け取れません。",
|
||||
"subscriptionDetail42": "有料プランに登録している間、1ヶ月間ログインしない場合、この特典はまだ受け取れますか?",
|
||||
"subscriptionDetail400": "現在の有料会員には、リリース後の最初の月の初回ログイン時に、最初のミスティック砂時計と毎月のジェム上限が+2増加します。つまり、もしすでに11月にログインしている場合、最初の定期的な増加は12月に行われます。",
|
||||
"subscriptionDetail40": "私は有料会員ですが、新しいスケジュールでは最初の定期的な神秘の砂時計とジェム上限の増加はいつ受け取れますか?",
|
||||
"subscriptionDetail420": "ミステリーギアセットと同様に、登録している間にログインしなくても神秘の砂時計やジェムの上限アップを見逃すことはありません。次回ログインしたときに、登録していた期間のすべての特典を受け取ることができます。",
|
||||
"subscriptionDetail43": "有料プランを申し込んでからキャンセルした場合でも、特典は受けられますか?"
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
"weaponSpecialFall2015WarriorText": "木の板",
|
||||
"weaponSpecialFall2015WarriorNotes": "とうもろこし畑で何かをもちあげるのに、そしてタスクをぶったたくのに役立ちます。力が<%= str %>上がります。2015年秋の限定装備。",
|
||||
"weaponSpecialFall2015MageText": "魔法の糸",
|
||||
"weaponSpecialFall2015MageNotes": "力のある刺繍の魔女は、この魔法の糸を手を触れずして操ります! 知能が<%= int %>、知覚が<%= per %>上がります。2015年秋の限定装備。",
|
||||
"weaponSpecialFall2015MageNotes": "力のある刺繍の魔女は、この魔法の糸を手を触れずして操ります!知能が<%= int %>、知覚が<%= per %>上がります。2015年秋の限定装備。",
|
||||
"weaponSpecialFall2015HealerText": "沼のスライムの毒薬",
|
||||
"weaponSpecialFall2015HealerNotes": "完璧に抽出されました!さあ、覚悟を決めて飲み干しましょう。知能が<%= int %>上がります。2015年秋の限定装備。",
|
||||
"weaponSpecialWinter2016RogueText": "ココアのマグカップ",
|
||||
@@ -555,7 +555,7 @@
|
||||
"armorSpecialSummer2016MageNotes": "このつるつるした服は、着る人を本物のイルカの魔道士に変えます!知能が<%= int %>上がります。2016年夏の限定装備。",
|
||||
"armorSpecialSummer2016HealerText": "タツノオトシゴのしっぽ",
|
||||
"armorSpecialSummer2016HealerNotes": "このツンツンした服は、着る人を本物のタツノオトシゴの治療師に変えます!体質が<%= con %>上がります。2016年夏の限定装備。",
|
||||
"armorSpecialFall2016RogueText": "黒婦人のよろい",
|
||||
"armorSpecialFall2016RogueText": "クロゴケグモのよろい",
|
||||
"armorSpecialFall2016RogueNotes": "このよろいについている瞳は、たえずまたたいています。知覚が<%= per %>上がります。2016年秋の限定装備。",
|
||||
"armorSpecialFall2016WarriorText": "スライムまみれのよろい",
|
||||
"armorSpecialFall2016WarriorNotes": "ミステリアスな湿り気とコケ!体質が<%= con %>上がります。2016年秋の限定装備。",
|
||||
@@ -991,7 +991,7 @@
|
||||
"headSpecialSummer2016MageNotes": "この帽子から魔法の水が止めどなく噴き出します。知覚が<%= per %>上がります。2016年夏の限定装備。",
|
||||
"headSpecialSummer2016HealerText": "タツノオトシゴのヘルメット",
|
||||
"headSpecialSummer2016HealerNotes": "このヘルメットをかぶっている人は、サキノバシティーのタツノオトシゴに訓練を受けたことを表します。知能が<%= int %>上がります。2016年夏の限定装備。",
|
||||
"headSpecialFall2016RogueText": "黒婦人のヘルメット",
|
||||
"headSpecialFall2016RogueText": "クロゴケグモのヘルメット",
|
||||
"headSpecialFall2016RogueNotes": "このヘルメットから生えている足は、たえずピクピクしています。知覚が<%= per %>上がります。2016年秋の限定装備。",
|
||||
"headSpecialFall2016WarriorText": "ふしくれだった樹皮のかぶと",
|
||||
"headSpecialFall2016WarriorNotes": "この沼につかっていたヘルメットは、泥土でおおわれています。 力が<%= str %>上がります。2016年秋の限定装備。",
|
||||
@@ -1363,7 +1363,7 @@
|
||||
"shieldSpecialWinter2017WarriorText": "パックの盾",
|
||||
"shieldSpecialWinter2017WarriorNotes": "巨大なホッケーパックから作られたこの盾は、かなりの衝撃に耐えられます。体質が<%= con %>上がります。2016-2017年冬の限定装備。",
|
||||
"shieldSpecialWinter2017HealerText": "シュガープラムの盾",
|
||||
"shieldSpecialWinter2017HealerNotes": "この噛みごたえのある装備は、涙がでるほど酸っぱいタスクからあなたを守ってくれます! 体質が<%= con %>上がります。2016-2017年冬の限定装備。",
|
||||
"shieldSpecialWinter2017HealerNotes": "この噛みごたえのある装備は、涙がでるほど酸っぱいタスクからあなたを守ってくれます!体質が<%= con %>上がります。2016-2017年冬の限定装備。",
|
||||
"shieldSpecialSpring2017WarriorText": "毛糸玉の盾",
|
||||
"shieldSpecialSpring2017WarriorNotes": "この盾の糸には、1本1本に防御魔法が編みこまれています! 遊んじゃいけませんよ(あんまりたくさんはね)。体質が<%= con %>上がります。2017年春の限定装備。",
|
||||
"shieldSpecialSpring2017HealerText": "バスケットの盾",
|
||||
@@ -2821,7 +2821,7 @@
|
||||
"armorArmoireBasketballUniformText": "バスケットボールのユニフォーム",
|
||||
"weaponArmoirePaintbrushText": "絵筆",
|
||||
"armorSpecialSpring2023HealerText": "ユリの葉のガウン",
|
||||
"armorArmoireBasketballUniformNotes": "このユニフォームの背中に何がプリントされているのか気になりますか?もちろん、それはあなたのラッキーナンバーです!知覚が<%= per %>、力が上がります。ラッキー宝箱:昔懐かしいバスケットボールセット(アイテム1/2)。",
|
||||
"armorArmoireBasketballUniformNotes": "このユニフォームの背中に何がプリントされているのか、気になりますか?もちろん、それはあなたのラッキーナンバーです!知覚が<%= per %>上がります。ラッキー宝箱:昔懐かしいバスケットボールセット(アイテム1/2)。",
|
||||
"headMystery202301Text": "勇敢な妖狐の耳",
|
||||
"backMystery202301Text": "武勇の五尾",
|
||||
"backMystery202302Notes": "このしっぽを付ければ、素敵な一日になること間違いなし!カルー!カライ!効果なし。2023年2月有料会員アイテム。",
|
||||
@@ -3161,8 +3161,8 @@
|
||||
"headMystery202503Text": "ジェイドのジャガーノートの髪",
|
||||
"eyewearMystery202503Text": "ジェイドのジャガーノートの目",
|
||||
"armorSpecialSummer2025MageText": "イトヒキベラのスーツ",
|
||||
"armorSpecialFall2025RogueNotes": "この季節的な装備は細くて、強くて、打ちにくいターゲットになる。知覚が<%= per %>上がります。2025年秋の限定装備。",
|
||||
"armorSpecialFall2025HealerNotes": "この季節的なよろいを着ると、暗い森の中に隠れて、戦略的に撤退することができる。体質が<%= con %>上がります。2025年秋の限定装備。",
|
||||
"armorSpecialFall2025RogueNotes": "この季節的な装備は細くて、強くて、打ちにくいターゲットになります。知覚が<%= per %>上がります。2025年秋の限定装備。",
|
||||
"armorSpecialFall2025HealerNotes": "この季節的なよろいを着ると、暗い森の中に隠れて、戦略的に撤退することができます。体質が<%= con %>上がります。2025年秋の限定装備。",
|
||||
"headSpecialSummer2025MageText": "イトヒキベラのヘッドドレス",
|
||||
"eyewearMystery202510Notes": "この不気味な目は名月のように輝きます。効果なし。2025年10月の有料会員アイテム。",
|
||||
"weaponSpecialFall2024WarriorText": "炎の剣",
|
||||
@@ -3170,5 +3170,22 @@
|
||||
"bodyMystery202509Notes": "このマフラーは風から守ってくれるだけでなく、見た目もとてもカッコいいです。効果なし。2025年9月の有料会員アイテム。",
|
||||
"headAccessoryMystery202410Text": "キャンディコーンの耳",
|
||||
"backMystery202505Text": "羽ばたくアゲハチョウの翼",
|
||||
"headAccessoryMystery202505Text": "羽ばたくアゲハチョウの触角"
|
||||
"headAccessoryMystery202505Text": "羽ばたくアゲハチョウの触角",
|
||||
"shieldSpecialFallRogue2024Text": "リボンの杖",
|
||||
"shieldSpecialSummer2024WarriorText": "ジンベエザメのヒレ",
|
||||
"shieldSpecialFall2024WarriorNotes": "この盾を使っていろいろな障害物を防ぐと、決心がつきます。体質が<%= con %>上がります。2024年秋の限定装備。",
|
||||
"headSpecialFall2024RogueText": "黒ネコの仮面",
|
||||
"headSpecialFall2024WarriorText": "ファイアインプの仮面",
|
||||
"headSpecialFall2025RogueText": "ガイコツの仮面",
|
||||
"headSpecialFall2024MageText": "冥界の魔術師の仮面",
|
||||
"headSpecialFall2024HealerText": "スペースインベーダーの仮面",
|
||||
"shieldSpecialFall2024HealerText": "宇宙の盾",
|
||||
"shieldSpecialFall2024WarriorText": "炎の盾",
|
||||
"headSpecialFall2025HealerText": "コボルドの仮面",
|
||||
"shieldSpecialFall2025HealerText": "コボルドの盾",
|
||||
"shieldArmoireHattersPocketWatchText": "ピカピカなポケットウォッチ",
|
||||
"shieldSpecialSummer2024HealerText": "海貝の盾",
|
||||
"shieldSpecialSummer2024HealerNotes": "このぴかぴかする盾は、海貝の杖よりも強いです。体質が<%= con %>上がります。2024年夏の限定装備。",
|
||||
"armorMystery202504Notes": "「汚れた雪男」、だって?本当は可愛らしいでしょ!効果なし。2025年4月の有料会員アイテム。",
|
||||
"armorArmoireSpringPetalYukataText": "春の花びらの浴衣"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"fall2016SwampThingSet": "沼地の怪物(戦士)",
|
||||
"fall2016WickedSorcererSet": "悪意の妖術師(魔道士)",
|
||||
"fall2016GorgonHealerSet": "ゴルゴン(治療師)",
|
||||
"fall2016BlackWidowSet": "黒婦人(盗賊)",
|
||||
"fall2016BlackWidowSet": "クロゴケグモ(盗賊)",
|
||||
"winter2017IceHockeySet": "アイスホッケー(戦士)",
|
||||
"winter2017WinterWolfSet": "冬オオカミ(魔道士)",
|
||||
"winter2017SugarPlumSet": "シュガープラム(治療師)",
|
||||
@@ -281,5 +281,6 @@
|
||||
"summer2025FairyWrasseMageSet": "イトヒキベラの魔導士セット",
|
||||
"fall2025SasquatchWarriorSet": "サスクワッチの戦士セット",
|
||||
"fall2025SkeletonRogueSet": "ガイコツの盗賊セット",
|
||||
"fall2025KoboldHealerSet": "コボルドの治療師セット"
|
||||
"fall2025KoboldHealerSet": "コボルドの治療師セット",
|
||||
"fall2025MaskedGhostMageSet": "覆面をした幽霊の魔導士セット"
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
"paymentAutoRenew": "この有料プランは解約するまで自動更新されます。有料プランを解約する必要がある場合は、設定から解約できます。",
|
||||
"paymentCanceledDisputes": "キャンセルの承認のメールをあなたに送りました。もしそのメールが見つからない場合は、今後の請求についての論争を防ぐために、私たちにご連絡をお願いいたします。",
|
||||
"cannotUnpinItem": "このアイテムはピン留めを外せません。",
|
||||
"paymentSubBillingWithMethod": "あなたの有料プランは、<br><strong><%= months %>ヶ月</strong>ごとに<strong><%= paymentMethod %></strong>によって <strong>$<%= amount %>.00 米ドル</strong> 請求されます。",
|
||||
"paymentSubBillingWithMethod": "あなたの有料プランは、<br><strong><%= months %>ヶ月</strong>ごとに<strong><%= paymentMethod %></strong>によって<strong>$<%= amount %>.00 米ドル</strong>請求されます。",
|
||||
"invalidUnlockSet": "このアイテムセットは無効なので、アンロックできません。",
|
||||
"nGems": "<%= nGems %>ジェム",
|
||||
"nMonthsSubscriptionGift": "<%= nMonths %>ヶ月の有料プラン(ギフト)",
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
"questEvilSantaDropBearCubPolarMount": "シロクマ(乗騎)",
|
||||
"questEvilSanta2Text": "子グマの捜索",
|
||||
"questEvilSanta2Notes": "猟師のサンタが乗騎のシロクマを捕まえたとき、子グマは、氷原に逃げていきました。たしかに森の中の氷の結晶の音に交じって、小枝をふむ音や雪がくだける音が聞こえます。足あとだ!それを追いかけようと走り出します。足あとと折れた小枝を見のがしてはいけません。子グマを見つけ出しましょう!<br><br><strong>注意</strong>:「子グマの捜索」のクエストは何回でも挑戦できますが、クエスト報酬の特別なペットが手に入るのは最初の一回だけです。",
|
||||
"questEvilSanta2Completion": "子グマを見つけました! ずっとあなたから離れないでしょう。",
|
||||
"questEvilSanta2Completion": "子グマを見つけました!ずっとあなたから離れないでしょう。",
|
||||
"questEvilSanta2CollectTracks": "足跡",
|
||||
"questEvilSanta2CollectBranches": "折れた小枝",
|
||||
"questEvilSanta2DropBearCubPolarPet": "シロクマ(ペット)",
|
||||
"questGryphonText": "炎のグリフォン",
|
||||
"questGryphonNotes": "偉大な猛獣使い、<strong>baconsaur</strong>があなたのパーティーに助けを求めてきました。「冒険者よ、どうか私を助けてください! 大切なグリフォンが逃げてしまい、Habit シティーを自由気ままに飛び回り、人びと に恐怖を与えているのです。もしグリフォンを止められたら、お礼にグリフォンのたまごを差し上げます!」",
|
||||
"questGryphonCompletion": "やりました! 強い獣はこそこそとはずかしそうに主人の元に帰りました。「驚いた! 冒険者よ、よくやってくれましたね!」 <strong>baconsaur</strong> は声を上げました。 「どうかこのグリフォンのたまごを受けとってください。あなたならきっとうまく育てられるでしょう!」",
|
||||
"questGryphonCompletion": "やりました!強い獣はこそこそとはずかしそうに主人の元に帰ります。「驚いた!冒険者よ、よくやってくれましたね!」<strong>baconsaur</strong> は声を上げます。 「どうかこのグリフォンのたまごを受けとってください。あなたならきっとうまく育てられるでしょう!」",
|
||||
"questGryphonBoss": "炎のグリフォン",
|
||||
"questGryphonDropGryphonEgg": "グリフォン ( たまご )",
|
||||
"questGryphonUnlockText": "市場でグリフォンのたまごを買えるようになります",
|
||||
@@ -815,5 +815,8 @@
|
||||
"questChameleonNotes": "タスクの森の暖かく雨の降る一角で、美しい一日が始まります。あなたは葉のコレクションに新しい蒐集物を探していると、目の前の枝が予告なしに色を変えました!しかも、その枝が動いたのです!<br><br>後ろにひっくり返りそうになりながら気づくと、それは枝ではなく巨大なカメレオンでした。体のあらゆる部分が色を変え続け、目はあちこちにキョロキョロと動いています。<br><br>「大丈夫ですか?」とあなたはカメレオンに尋ねます。<br><br>「うーん、ええと…」と、少しあわてた様子で彼は答えます。「隠れようとしているんだけど…色が次々に現れては消えるから圧倒されちゃって…ひとつに集中するのが難しいんだ…」<br><br>「なるほど」とあなた。「それなら手伝えるかも。小さなチャレンジで集中力を鍛えよう!色の準備はいい?」<br><br>「任せて!」とカメレオンは答えました。",
|
||||
"questPlatypusRageEffect": "完璧主義者のカモノハシは川に潜って、パーティーに水をかけます!パーティーのマナが減ってしまいました!",
|
||||
"questOpalCollectLibraRunes": "てんびん座のルーン",
|
||||
"questOpalCollectMercuryRunes": "水星のルーン"
|
||||
"questOpalCollectMercuryRunes": "水星のルーン",
|
||||
"questPlatypusNotes": "コンクエスト入り江の天気は晴れているが、宿題プリントのせいで台無しになっています。なんでいつも冒険したいときに宿題があるんだろう、とあなたは考えます。河川の生態系についての問題を解いていると、作文が出てきます。<br><br>「動物は川に住むためにどのように工夫しているか?知らんよ...」<br><br>どこから始めたらいいのかもわからず戸惑っていると、下流からバタバタする音が聞こえてきます。<br><br>「やれやれ」と、水面のしたからため息。すると、疲れたカモノハシが浮かび上がります。「全然巣穴が進まない。どうしても思い通りに作れないわ。」彼女は再び水中に潜り込み、幅広い尾が水面にあたるとあなたに大きな水しぶきがかかります。<br><br>「ちょっと待って、全部崩さないで!」とあなたは叫びます。早く助けてあげないと!(ついでに作文のアイデアが出るかも!)",
|
||||
"questCatRageTitle": "怒りの猫パンチ",
|
||||
"questRaccoonRageEffect": "欲張りなアライグマはあなたが救出したアイテムを奪い取り、木の中に詰め込みます。ボスは体力を30%回復してしまいました!"
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
"subscriptionBenefit3": "毎日2倍のタマゴ、たまごがえしの薬と餌を見つけてペットコレクションを増やしましょう!",
|
||||
"subscriptionBenefit4": "最新の限定装備で着飾りましょう。有料プランに登録して<%= month %>の <%= currentMysterySetName %>をゲットしよう!",
|
||||
"subscriptionBenefit5": "さっそく有料プランに登録して、特別なロイヤルパープルのジャッカロープのペットをゲットしよう!",
|
||||
"subscriptionBenefit6": "タイムトラベラーの店でアイテムを買うために神秘の砂時計を手に入れましょう!",
|
||||
"subscriptionBenefit6": "タイムトラベラーの店で使える神秘の砂時計を毎月1個ゲットして、アイテムを見逃さないようにしよう!",
|
||||
"purchaseAll": "セットを購入する",
|
||||
"gemsRemaining": "残り",
|
||||
"notEnoughGemsToBuy": "今月はこれ以上購入できるジェムはありません。毎月最初の 3 日以内にさらに多く購入可能になります。",
|
||||
@@ -263,5 +263,11 @@
|
||||
"mysterySet202511": "霜の戦士セット",
|
||||
"recurringNMonthly": "<%= length %>ヶ月ごとに更新されます",
|
||||
"mysterySet202510": "空中に浮かぶグールセット",
|
||||
"unlockNGemsGift": "受取人は毎月<strong><%= count %> 個のジェム</strong>をアンロックします"
|
||||
"unlockNGemsGift": "受取人は毎月<strong><%= count %> 個のジェム</strong>をアンロックします",
|
||||
"earn2GemsGift": "受取人は有料プランに登録している間、毎月<strong>+2個のジェム</strong>を獲得します",
|
||||
"earn2Gems": "登録している間、毎月<strong>+2個のジェム</strong>を獲得",
|
||||
"mysterySet202502": "親切なアルレッキーノセット",
|
||||
"maxGemCapGift": "受取人は<strong>最大限のジェム</strong>をゲットします",
|
||||
"subscribeAgainContinueHourglasses": "神秘の砂時計をゲットし続けるには、再び有料プランに登録してください",
|
||||
"immediate12Hourglasses": "初めて12ヶ月間の有料プランに登録したとき、<strong>12個の神秘の砂時計</strong> をすぐにゲットしよう!"
|
||||
}
|
||||
|
||||
@@ -773,5 +773,7 @@
|
||||
"backgroundInsideACrystalNotes": "Kijk vanuit een Kristal naar buiten.",
|
||||
"backgroundSnowyVillageText": "Besneeuwd Dorp",
|
||||
"backgroundSnowyVillageNotes": "Bewonder een Besneeuwd Dorp.",
|
||||
"backgrounds122022": "SET 103: Uitgebracht december 2022"
|
||||
"backgrounds122022": "SET 103: Uitgebracht december 2022",
|
||||
"backgroundSpringtimeShowerText": "Lentedouche",
|
||||
"backgroundSpringtimeShowerNotes": "Zie een bloemrijke lentedouche."
|
||||
}
|
||||
|
||||
@@ -1772,7 +1772,7 @@
|
||||
"eyewearArmoireComedyMaskText": "Komediowa Maska",
|
||||
"eyewearArmoireJewelersEyeLoupeText": "Lupa Jubilerska",
|
||||
"moreArmoireGearAvailable": "Do tego czasu, zostało do znalezienia <%= armoireCount %> części wyposażenia w Zaczarowanej Skrzyni!",
|
||||
"gearItemsCompleted": "Jesteś w posiadaniu całego ekwipunku klasy <%= class %>! Nowy ekwipunek jest wydawany podczas Gal sezonowych.",
|
||||
"gearItemsCompleted": "Jesteś w posiadaniu całego ekwipunku klasy <%= klass %>! Nowy ekwipunek jest wydawany podczas Gal sezonowych.",
|
||||
"moreArmoireGearComing": "Zaczarowana Skrzynia również comiesięcznie otrzymuje nowy asortyment!",
|
||||
"weaponSpecialFall2019MageNotes": "Niezależnie od tego, czy chodzi o wykuwanie piorunów, wznoszenie fortyfikacji, czy po prostu wzbudzanie przerażenia w sercach śmiertelników, ta laska daje moc gigantów do czynienia cudów. Zwiększa inteligencję o <%= int %> i percepcję o <%= per %>. Limitowana edycja 2019 Jesienne Wyposażenie.",
|
||||
"weaponSpecialFall2019HealerText": "Przerażające Filakterium",
|
||||
|
||||
@@ -900,5 +900,20 @@
|
||||
"backgroundMountainSceneWithBlossomsNotes": "Aprecie as paisagens encantadoras e os cheiros da Paisagem de Montanha com Flores.",
|
||||
"backgrounds0420205": "Conjunto 131: Lançamento de Abril 2025",
|
||||
"backgroundGardenWithFlowerBedsText": "Jardim com Cama de Flores",
|
||||
"backgroundGardenWithFlowerBedsNotes": "Participe do florescer da primavera em um Jardim com Cama de Flores."
|
||||
"backgroundGardenWithFlowerBedsNotes": "Participe do florescer da primavera em um Jardim com Cama de Flores.",
|
||||
"backgrounds082025": "KIT 135: Lançado em Agosto de 2025",
|
||||
"backgrounds092025": "KIT 136: Lançado em Setembro de 2025",
|
||||
"backgrounds102025": "KIT 137: Lançado em Outubro de 2025",
|
||||
"backgroundInsideForestWitchsCottageText": "Cabana da Bruxa da Floresta",
|
||||
"backgroundAutumnSwampNotes": "Absorva as vibrações assombrosas de um Pântano de Outono.",
|
||||
"backgroundAutumnSwampText": "Pântano de Outono",
|
||||
"backgroundInsideForestWitchsCottageNotes": "Lance feitiços dentro da Cabana da Bruxa da Floresta.",
|
||||
"backgroundCastleKeepWithBannersText": "Castelo adornado com estandartes",
|
||||
"backgrounds112025": "KIT 138: Lançado em Novembro de 2025",
|
||||
"backgroundCastleKeepWithBannersNotes": "Cante histórias de feitos heroicos em um Salão do Castelo adornado com estandartes.",
|
||||
"backgrounds062025": "KIT 133: Lançado em Junho de 2025",
|
||||
"backgroundTrailThroughAForestText": "Trilha pela Floresta",
|
||||
"backgroundTrailThroughAForestNotes": "Percorra uma trilha pela floresta.",
|
||||
"backgrounds052025": "KIT 132: Lançado em Maio de 2025",
|
||||
"backgrounds072025": "KIT 134: Lançado em Julho de 2025"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"commonQuestions": "Dúvidas Comuns",
|
||||
"faqQuestion25": "Quais são os diferentes tipos de tarefas?",
|
||||
"webFaqAnswer25": "O Habitica utiliza três tipos diferentes de tarefas para acomodar as suas necessidades: Hábitos, Diárias e Afazeres.\n\nHábitos podem ser positivos ou negativos e representam algo que você gostaria de acompanhar ao longo de vários momentos do dia ou fora de uma agenda estruturada. Hábitos Positivos garantem recompensas, como Ouro e Experiência (EXP), enquanto Hábitos Negativos causam dano a você, fazendo com que você perca pontos de vida.\n\nDiárias são tarefas que se repetem e que devem ser concluídas em uma agenda bem estruturada, como, por exemplo, uma vez ao dia, três vezes na semana ou quatro vezes em um mês. Não completar as suas Diárias causa dano a você e ao seu Grupo, fazendo com que vocês percam pontos de vida. Por outro lado, quanto maior a dificuldade das suas Diárias, melhores serão as recompensas!\n\nAfazeres são tarefas que ocorrem apenas uma vez e que garantem recompensas imediatamente após a sua conclusão. Afazeres podem ter uma data limite estipulada, mas não irão causar dano a você ou ao seu Grupo caso não sejam concluídos dentro do prazo.\n\nEscolha o tipo de tarefa que melhor se enquadra a cada um dos objetivos que você deseja atingir!",
|
||||
"faqQuestion26": "Quais seriam algumas tarefas de exemplo?",
|
||||
"faqQuestion26": "Quais são algumas tarefas de exemplo?",
|
||||
"webFaqAnswer26": "Hábitos Positivos (Comportamentos que você quer incentivar; apresenta sinal \"de mais/adição\")\n\n * Tomar suas vitaminas\n * Passar fio dental\n * Estudar por uma hora\n\nHábitos Negativos (Comportamentos que você quer evitar parcialmente ou completamente; apresenta botão com sinal \"de menos/subtração\")\n\n * Fumar\n * Perder tempo no feed de redes sociais\n * Roer unhas\n\nHábitos Ambíguos (Hábitos que envolvem a escolha entre ações positivas e negativas; apresenta ambos os botões positivos e negativos)\n\n * Beber água x Beber Refrigerante\n * Estudar x Procrastinar\n\nExemplos de Tarefas Diárias (Tarefas que você deve fazer em determinada frequência)\n * Lavar a louça\n * Regar as plantas\n * 30 minutos de atividade física\n\nExemplos de Afazeres (Tarefas que você vai realizar uma única vez)\n\n * Agendar uma consulta médica\n * Organizar o guarda roupa\n * Terminar o TCC",
|
||||
"faqQuestion27": "Por que as tarefas mudam de cor?",
|
||||
"webFaqAnswer27": "A cor de uma tarefa é uma representação visual de seu valor. Todas as tarefas começam como amarelas (neutras), azuis são melhores e vermelhas são piores. Veja como cada tipo de tarefa determina seu valor:\n\nHábitos tornam-se mais azuis ou vermelhos conforme você toca no botão de mais ou menos. Hábitos positivos e negativos degradam para amarelo ao longo do tempo se você não os completar. Hábitos duplos só mudam de cor com base em suas entradas.\n\nDiárias mudam de cor com base na frequência que são concluídos, ficando mais azuis conforme são concluídos ou mais vermelhos se forem perdidos.\n\nAfazeres ficam gradualmente mais vermelhos quanto mais tempo permanecerem incompletos.\n\nQuanto mais vermelha estiver a tarefa, mais Ouro e Experiência você ganhará ao concluí-la, então certifique-se de completar até mesmo suas tarefas mais difíceis!",
|
||||
@@ -19,15 +19,15 @@
|
||||
"webFaqAnswer30": "Se seus Pontos de Vida chegarem a zero, você perderá um nível, todo o seu Ouro e uma peça de Equipamento que pode ser recomprada.",
|
||||
"faqQuestion31": "Por que perdi Pontos de Vida ao interagir com uma tarefa não negativa?",
|
||||
"webFaqAnswer31": "Se você completar uma tarefa e perder Pontos de Vida quando não deveria, você encontrou um atraso enquanto o servidor está sincronizando as alterações feitas em outras plataformas. Por exemplo, se você usar Ouro, Mana ou perder Pontos de Vida no aplicativo para celular e depois completar uma tarefa no site, o servidor está apenas confirmando que tudo está sincronizado.",
|
||||
"faqQuestion32": "Como posso escolher uma classe?",
|
||||
"faqQuestion32": "Como eu posso escolher uma classe?",
|
||||
"webFaqAnswer32": "Todos os jogadores começam como Guerreiro até atingirem o nível 10. Depois de alcançar o nível 10, você terá a opção de escolher entre selecionar uma nova classe ou continuar como Guerreiro.\n\nCada classe possui Equipamentos e Habilidades diferentes. Se você não quiser escolher uma classe, pode selecionar \"Abster-se\". Se optar por abster-se, você sempre poderá ativar o Sistema de Classes nas Configurações mais tarde.\n\nSe você quiser trocar sua classe após o nível 10, é possível através do uso do Orbe do Renascimento. O Orbe do Renascimento é disponibilizado no Mercado por 6 gemas quando você atinge o nível 50 ou de graça quando você atinge o nível 100.\n\nComo alternativa, você pode trocar de classe a qualquer momento nas Configurações por 3 gemas. Isso não irá reiniciar seus níveis como o Orbe do Renascimento, mas você poderá realocar seus pontos de habilidades acumulados para adaptar-los a sua nova classe.",
|
||||
"faqQuestion33": "O que é a barra azul que aparece após o nível 10?",
|
||||
"webFaqAnswer33": "Após desbloquear o Sistema de Classes, você também desbloqueia Habilidades que requerem Mana para serem lançadas. A Mana é determinada pelo seu atributo INT e pode ser ajustada por meio de Habilidades e Equipamentos.",
|
||||
"faqQuestion34": "Que tipo de Comida meu Mascote gosta?",
|
||||
"faqQuestion35": "Alimentei meu Mascote e ele desapareceu! O que aconteceu?",
|
||||
"webFaqAnswer35": "Depois de alimentar o seu Mascote o suficiente para transformá-lo em uma Montaria, será necessário eclodir esse tipo de Mascote novamente para tê-lo em seu Estábulo.\n\nPara visualizar Montarias no aplicativo:\n\n No Menu, selecione \"Mascotes & Montarias\" e mude para a guia de Montarias.\n\nPara visualizar Montarias no site:\n\n * No menu Inventário, selecione \"Estábulo\" e role para baixo até a seção de Montarias",
|
||||
"webFaqAnswer35": "Depois de alimentar o seu Mascote o suficiente para transformá-lo em uma Montaria, será necessário eclodir esse tipo de Mascote novamente para tê-lo em seu Estábulo.\n\nPara visualizar Montarias no aplicativo:\n\n No Menu, selecione \"Mascotes E Montarias\" e mude para a guia de Montarias.\n\nPara visualizar Montarias no site:\n\n * No menu Inventário, selecione \"Estábulo\" e role para baixo até a seção de Montarias",
|
||||
"faqQuestion36": "Como eu mudo a aparência do meu Avatar?",
|
||||
"webFaqAnswer36": "Existem inúmeras maneiras de personalizar a aparência do seu Avatar no Habitica! Você pode alterar a forma do corpo, o estilo e a cor do cabelo, a cor da pele, adicionar óculos ou auxíliadores de mobilidade, selecionando “Personalizar” (ou \"Editar\") Avatar no menu.\n\nPara personalizar o seu Avatar no aplicativo:\n *No menu, selecione “Personalizar Avatar”\n\nPara personalizar o seu Avatar no site:\n *No menu de usuário na barra de navegação, selecione “Editar Avatar”",
|
||||
"webFaqAnswer36": "Existem inúmeras maneiras de personalizar a aparência do seu Avatar no Habitica! Você pode alterar a forma do corpo, o estilo e a cor do cabelo, a cor da pele ou adicionar óculos, ou auxiliadores de mobilidade, selecionando “Personalizar” (ou \"Editar\") Avatar no menu.\n\nPara personalizar o seu Avatar no aplicativo:\n *No menu, selecione “Personalizar Avatar”\n\nPara personalizar o seu Avatar no site:\n *No menu de usuário na barra de navegação, selecione “Editar Avatar”",
|
||||
"faqQuestion37": "Por que meu Equipamento não está sendo exibido no meu Avatar?",
|
||||
"faqQuestion38": "Por que não consigo comprar determinados itens?",
|
||||
"webFaqAnswer37": "Verifique se a opção de Fantasia está ativada. Se o seu Avatar estiver usando uma Fantasia, esse Conjunto de Equipamentos será exibido em vez do seu Equipamento de Batalha.\n\nPara ativar ou desativar a Fantasia no aplicativo:\n * No menu, selecione “Equipamento” para encontrar a opção de Fantasia\n\nPara ativar ou desativar a Fantasia no site:\n * No seu Inventário, selecione “Equipamento” e localize a opção de Fantasia na guia de Fantasias do menu de Equipamentos",
|
||||
@@ -54,7 +54,7 @@
|
||||
"faqQuestion48": "Posso jogar Habitica com outras pessoas?",
|
||||
"webFaqAnswer48": "Sim, com Grupos! Você pode iniciar seu próprio Grupo ou ingressar em um existente. Fazer parte de um Grupo com outros jogadores do Habitica é uma ótima maneira de embarcar em Missões, receber benefícios das habilidades dos membros do Grupo e aumentar sua motivação com responsabilidade adicional.",
|
||||
"faqQuestion49": "Como encontro um Grupo quando não estou em nenhum?",
|
||||
"webFaqAnswer49": "Se você deseja vivenciar o Habitica com outras pessoas, mas não conhece outros jogadores, procurar por um Grupo é a melhor opção! Se você já conhece outros jogadores que têm um Grupo, pode compartilhar seu nome de usuário com eles para ser convidado. Alternativamente, você pode criar um novo Grupo e convidá-los com seu nome de usuário ou endereço de e-mail.\n\nPara criar ou procurar um Grupo, selecione \"Grupo\" no menu de navegação e escolha a opção que melhor funciona para você.",
|
||||
"webFaqAnswer49": "Se você deseja vivenciar o Habitica com outras pessoas, mas não conhece outros jogadores, procurar por um Grupo é a melhor opção! Se você já conhece outros jogadores que têm um Grupo, pode compartilhar seu nome de usuário com eles para ser convidado. Alternativamente, você pode criar um novo Grupo e convidá-los com seu nome de usuário ou endereço de e-mail.\n\nPara criar ou procurar um Grupo, selecione \"Grupo\" no menu de navegação e escolha a opção que funciona para você.",
|
||||
"faqQuestion50": "Como funciona a busca por um Grupo?",
|
||||
"webFaqAnswer50": "Após selecionar “Procurar um Grupo”, você será adicionado a uma lista de jogadores que desejam entrar em um Grupo. Os líderes do Grupo podem visualizar essa lista e enviar convites. Assim que receber um convite, você pode aceitá-lo nas suas notificações para ingressar no Grupo de sua escolha!\n\nVocê pode receber vários convites para diferentes Grupos. No entanto, você só pode ser membro de um Grupo por vez.",
|
||||
"faqQuestion51": "Por quanto tempo posso procurar por um Grupo após entrar na lista?",
|
||||
@@ -117,7 +117,7 @@
|
||||
"faqQuestion65": "Os Planos de Time são suportados nos aplicativos móveis?",
|
||||
"faqQuestion66": "Qual é a diferença entre as tarefas compartilhadas de um Plano de Time e as tarefas de Desafio?",
|
||||
"sunsetFaqTitle": "FAQ sobre a Descontinuação do Serviço de Taverna e Guildas do Habitica",
|
||||
"webFaqAnswer60": "Aqui estão algumas dicas para começar com seu novo Plano de Grupo Habitica:\n\n * Promova um membro a gerente para dar a ele a habilidade de criar e editar tarefas\n * Deixe tarefas não-designadas se qualquer um puder completá-la e ela só precisa ser feita uma vez\n * Designe uma tarefa a uma pessoa para ter certeza que ninguém mais completará a tarefa dela\n * Designe uma tarefa para múltiplas pessoas se todos eles precisarem completá-la *Ative a habilidade que mostra tarefas compartilhas no seu mural pessoal para não esquecer de nada\n * Você é recompensado pelas tarefas que você completa, mesmo as multi-designadas\n * Recompensas por completar tarefas não são divididas entre os membros.\n * Use as cores de tarefas no mural do time para avaliar a frequência média de tarefas concluídas.\n * Regularmente revise as tarefas no mural de tarefas compartilhado para ter certeza que elas continuam relevantes.\n * Deixar de fazer uma Diária não dará dano a você ou ao seu time, mas a cor da missão vai decair",
|
||||
"webFaqAnswer60": "Aqui estão algumas dicas para começar com seu novo Plano de Grupo Habitica:\n\n * Promova um membro a gerente para dar a ele a habilidade de criar e editar tarefas\n * Deixe tarefas não-designadas se qualquer um puder completá-la e ela só precisa ser feita uma vez\n * Designe uma tarefa a uma pessoa para ter certeza que ninguém mais completará a tarefa dela\n * Designe uma tarefa para múltiplas pessoas se todos eles precisarem completá-la \n*Ative a habilidade que mostra tarefas compartilhas no seu mural pessoal para não esquecer de nada\n * Você é recompensado pelas tarefas que você completa, mesmo as multi-designadas\n * Recompensas por completar tarefas não são divididas entre os membros.\n * Use as cores de tarefas no mural do time para avaliar a frequência média de tarefas concluídas.\n * Regularmente revise as tarefas no mural de tarefas compartilhado para ter certeza que elas continuam relevantes.\n * Deixar de fazer uma Diária não dará dano a você ou ao seu time, mas a cor da missão vai decair",
|
||||
"webFaqAnswer65": "Enquanto os aplicativos móveis ainda não suportam totalmente a funcionalidade de Plano de Time, até lá você pode completar as tarefas compartilhadas dos aplicativos de iOS e Android!\n\nNo Android, você pode tocar no seu nome mostrado no topo da tela enquanto estiver vendo suas tarefas para mudar para o quadro de tarefas compartilhadas. A partir daí, você pode ver os membros, acessar o bate-papo, e criar, completar, ou atribuir tarefas.\n\nVocê também pode ativar a preferência para copiar tarefas compartilhadas para seu quadro de tarefas pessoal, para que assim você possa completar todas suas tarefas de um só lugar.\n\nPara fazer isso em aplicativos móveis:\n *Abra as Configurações e ative \"Copie as tarefas compartilhadas\"\n\nPara fazer isso no website do Habitica:\n *Navegue até o seu Plano de TIme e ative o botão de \"Copiar tarefas\" no quadro de afazeres compartilhados",
|
||||
"webFaqAnswer64": "Tarefas compartilhadas irão reiniciar ao mesmo tempo para todos para manter o quadro de tarefas compartilhadas em sincronia. Esse tempo pode ser visto no quadro de tarefas compartilhadas, e é determinado pela hora de início do dia do líder do Plano de Time. Pelo fato de que as tarefas compartilhadas reiniciam automaticamente, você não terá a chance de completar as diárias compartilhadas incompletas do dia anterior quando conferir na manhã seguinte.\n\nDiárias compartilhadas não causarão dano se não forem completadas, no entanto, sua cor degradará para ajudar na visualização do progresso.",
|
||||
"webFaqAnswer66": "O quadro de afazeres compartilhados do Plano de Time são mais dinâmicos que os Desafios, pois eles podem ser atualizados constantemente, além de serem interativos. Desafios são ótimos se você tem um conjunto de tarefas para ser mandado para várias pessoas.\n\nOs Planos de Time são uma funcionalidade paga, enquanto os Desafios são disponibilizados de graça para todos.\n\nVocê não pode atribuir tarefas específicas nos Desafios, e os Desafios não possuem um dia compartilhado para reinício. No geral, Desafios oferecem menos controle e interação direta.",
|
||||
@@ -149,7 +149,7 @@
|
||||
"contentQuestion1": "Por que o Habitica está fazendo essas mudanças?",
|
||||
"contentAnswer12": "Os jogadores terão mais facilidade para completar suas coleções com os itens sendo lançados em um cronograma mais previsível.",
|
||||
"contentQuestion2": "De que forma as Festas de Gala estão mudando?",
|
||||
"contentAnswer03": "Fundos, Cores e Estilos de Cabelo, Peles, Acessórios Animais e Camisas agora poderão ser adquiridos na nova <strong>Loja de Customização!</strong>",
|
||||
"contentAnswer03": "Planos de fundo, Cores e Estilos de Cabelo, Peles, Acessórios Animais e Camisas agora poderão ser adquiridos na nova <strong>Loja de Customização!</strong>",
|
||||
"contentAnswer20": "Sempre haverá Festas de Gala ativas quando as mudanças de programação começarem.",
|
||||
"contentAnswer200": "<strong>Explosão de Verão</strong>: 21 de junho a 20 de setembro",
|
||||
"contentAnswer201": "<strong>Festival de Outono</strong>: 21 de setembro a 20 de dezembro",
|
||||
@@ -164,7 +164,7 @@
|
||||
"contentAnswer22": "As Poções Mágicas de Eclosão não estarão mais associadas às Galas e, em vez disso, seguirão um cronograma de lançamento mensal próprio, com tema nas festividades em andamento.",
|
||||
"contentAnswer30": "As lojas irão alternar uma seleção de seus itens a cada mês. Isso contribuirá para que a quantidade de conteúdo nas lojas seja mais organizada e simples de explorar. O novo cronograma oferecerá itens inéditos para os jogadores novos, enquanto cria um cronograma previsível para os colecionadores veteranos.",
|
||||
"subscriptionDetail47": "Eu tenho uma assinatura de Plano de time, como isso me afeta?",
|
||||
"contentAnswer303": "<strong>No dia 21 de cada mês:</strong> As Poções de Incubação Mágicas disponíveis na Loja são trocadas.",
|
||||
"contentAnswer303": "<strong>No dia 21 de cada mês:</strong> As Poções de Incubação Mágicas disponíveis na Loja são alteradas.",
|
||||
"contentQuestion4": "Que conteúdo novo está chegando?",
|
||||
"contentAnswer400": "Missões de Companheiros",
|
||||
"contentAnswer403": "Cores de cabelos do verão",
|
||||
@@ -179,8 +179,8 @@
|
||||
"contentAnswer62": "As Poções Mágicas de Incubação do Dia dos Namorados agora estão incluídas na programação mensal.",
|
||||
"contentQuestion7": "E quanto aos outros itens disponíveis na Loja dos Viajantes do Tempo além dos Conjuntos de Assinantes anteriores?",
|
||||
"contentAnswer71": "Fique ligado para mais atualizações sobre melhorias planejadas para a experiência da Loja dos Viajantes do Tempo.",
|
||||
"faqQuestion67": "Quais são as classes no Habitica?",
|
||||
"contentAnswer40": "Para preencher este novo cronograma, temos trabalhado duro criando novos itens em uma variedade de categorias, incluindo:",
|
||||
"faqQuestion67": "O que são as classes no Habitica?",
|
||||
"contentAnswer40": "Para preencher essa nova programação, temos trabalhado arduamente criando novos itens em uma variedade de categorias, incluindo:",
|
||||
"contentAnswer401": "Missões de Poções Mágicas de Incubação",
|
||||
"contentAnswer402": "Poções Mágicas de Incubação",
|
||||
"contentAnswer52": "Esperamos que essa mudança ajude os jogadores a classificar as personalizações que possuem ao editar a aparência de seus avatares, ao mesmo tempo em que mantêm a experiência familiar da loja para outros itens compráveis.",
|
||||
@@ -189,11 +189,59 @@
|
||||
"contentAnswer70": "Papéis de Fundo, missões, mascotes e montarias disponíveis na Loja dos Viajantes do Tempo permanecerão disponíveis o ano todo.",
|
||||
"subscriptionBenefitsAdjustments": "Ajustes de Benefícios para Assinantes",
|
||||
"subscriptionBenefitsFaqTitle": "Perguntas frequentes sobre ajustes de benefícios para assinantes",
|
||||
"contentAnswer302": "<strong>No dia 14 de cada mês:</strong> Missões de Companheiros, Missões de Poções e Pacotes de Missões disponiveis na Loja de Missões são trocadas.",
|
||||
"contentAnswer302": "<strong>No dia 14 de cada mês:</strong> Missões de Mascotes, Missões de Poções e Pacotes de Missões disponíveis na Loja de Missões são alterados.",
|
||||
"contentFaqPara3": "Caso tenha alguma dúvida que não esteja nas respostas acima, entre em contato com nossa equipe pelo e-mail <%= mailto %>! Estamos animados com este novo cronograma de lançamento de conteúdo e ansiosos por mais projetos no futuro para ajudar a tornar o Habitica melhor para todos os jogadores.",
|
||||
"webFaqAnswer67": "Classes são diferentes papéis que seu personagem pode desempenhar. Cada classe oferece seu próprio conjunto de benefícios e habilidades únicas conforme você sobe de nível. Essas habilidades podem complementar a forma como você interage com suas tarefas ou ajudar a contribuir para completar missões em seu grupo.\n\nSua classe também determina o equipamento que estará disponível para você comprar nas suas recompensas, no mercado e na loja sazonal.\n\nAqui está um resumo de cada classe para ajudar você a escolher qual delas se encaixa melhor no seu estilo de jogo:\n#### **Guerreiro**\n*Guerreiros causam alto dano a chefes e têm uma grande chance de acertos críticos ao completar tarefas, recompensando você com experiência e ouro extra.\n*Força é seu atributo principal, aumentando o dano causado.\n*Constituição é seu atributo secundário, reduzindo o dano recebido.\n*As habilidades dos Guerreiros aumentam a constituição e a força dos colegas de grupo.\n*Considere jogar como Guerreiro se você gosta de enfrentar chefes, mas também quer alguma proteção caso perca tarefas de vez em quando.\n#### **Curandeiro**\nCurandeiros têm alta defesa e podem curar a si mesmos e aos colegas de grupo.\n*Constituição é seu atributo principal, aumentando suas curas e reduzindo o dano recebido.\n*Inteligência é seu atributo secundário, aumentando sua mana e experiência.\n*As habilidades dos Curandeiros tornam suas tarefas menos vermelhas e aumentam a constituição dos colegas de grupo.\n*Considere jogar como Curandeiro se você costuma perder tarefas e precisa da habilidade de se curar ou curar membros do grupo. Curandeiros também sobem de nível rapidamente.\n#### **Mago**\n*Magos sobem de nível rapidamente, ganham muita mana e causam dano a chefes em missões.\n*Inteligência é seu atributo principal, aumentando sua mana e experiência.\n*Percepção é seu atributo secundário, aumentando seu ouro e a chance de encontrar itens.\n*As habilidades dos Magos congelam sequências de tarefas, restauram a mana dos colegas de grupo e aumentam sua Inteligência.\n*Considere jogar como Mago se você se motiva ao progredir rapidamente pelos níveis e contribuir com dano em missões contra chefes.\n#### **Ladino**\n*Ladinos obtêm mais itens e ouro ao completar tarefas, e têm grande chance de acertos críticos, ganhando ainda mais experiência e ouro.\n*Percepção é seu atributo principal, aumentando seu ouro e a chance de encontrar itens.\n*Força é seu atributo secundário, aumentando o dano causado.\n*As habilidades dos Ladinos ajudam a evitar consequências por tarefas diárias não cumpridas, roubam ouro e aumentam a percepção dos colegas de grupo.\n*Considere jogar como Ladino se você se motiva muito por recompensas.",
|
||||
"subscriptionDetail00": "Todos os assinantes, incluindo aqueles com assinaturas de presente, receberão 1 Ampulheta Mística no início de cada mês em que tiverem os benefícios de assinante.",
|
||||
"subscriptionDetail000": "Assinar por 12 meses agora te dá 12 Ampulhetas Místicas, em vez das 4 de antes.",
|
||||
"subscriptionPara0": "Estamos deixando as assinaturas do Habitica ainda melhores, com mais Ampulhetas Místicas e Gemas! Com essas mudanças, ficou muito mais fácil entender os benefícios da sua assinatura.",
|
||||
"subscriptionHeading0": "Mudanças nas Ampulhetas Místicas"
|
||||
"subscriptionHeading0": "Mudanças nas Ampulhetas Místicas",
|
||||
"subscriptionDetail001": "Todos os assinantes receberão Ampulhetas Místicas na mesma data, correspondendo ao cronograma de lançamento dos Conjuntos de Equipamentos Misteriosos mensais.",
|
||||
"subscriptionDetail002": "Os assinantes não precisarão mais esperar até o mês seguinte ao pagamento recorrente para receber as Ampulhetas Místicas.",
|
||||
"subscriptionDetail003": "Todos os novos assinantes receberão 1 Ampulheta Mística imediatamente após a compra. Essa ampulheta contará como a entrega de Ampulheta Mística do mês atual.",
|
||||
"subscriptionDetail01": "Novas assinaturas recorrentes de 12 meses receberão um bônus inicial único de 12 Ampulhetas Místicas extras no momento da compra.",
|
||||
"subscriptionDetail010": "Isso será um acréscimo à Ampulheta Mística mensal que todos os novos assinantes recebem após a compra inicial.",
|
||||
"subscriptionDetail011": "Jogadores atuais com uma assinatura ativa de 12 meses receberão este bônus no dia em que essas alterações entrarem em vigor.",
|
||||
"subscriptionDetail10": "A quantidade de Gemas que os assinantes podem comprar com Ouro no Mercado aumentará em 2 a cada mês que eles tiverem benefícios, até atingir um máximo de 50.",
|
||||
"subscriptionDetail100": "As novas assinaturas de 1, 3 e 6 meses agora começarão com 24 Gemas por mês e esse valor aumentará a cada mês que houver benefícios.",
|
||||
"subscriptionDetail101": "Qualquer assinante que atualmente tenha um número ímpar de Gemas por mês terá seu limite de Gemas arredondado para o número par mais próximo.",
|
||||
"subscriptionDetail012": "Este bônus não se aplica a assinaturas presenteadas.",
|
||||
"subscriptionHeading1": "Mudanças nas Gemas dos Assinantes",
|
||||
"subscriptionDetail102": "As novas assinaturas de 12 meses começarão imediatamente com a quantidade máxima de Gemas por mês, 50 Gemas em vez das 45 anteriores.",
|
||||
"subscriptionDetail11": "A quantidade de Gemas que você pode comprar mensalmente por Ouro não será mais zerada se sua assinatura acabar.",
|
||||
"subscriptionHeading2": "Por que estamos fazendo estas mudanças?",
|
||||
"subscriptionDetail20": "Com a estrutura atual, pode ser difícil entender quantas Ampulhetas Místicas você receberá e quando.",
|
||||
"subscriptionDetail22": "Assinaturas presenteadas e recorrentes apresentavam alguns conflitos em relação a benefícios e regras que queríamos simplificar.",
|
||||
"subscriptionDetail23": "Fornecer uma Ampulheta Mística por mês permite que os assinantes aproveitem os itens rotativos na Loja dos Viajantes do Tempo.",
|
||||
"subscriptionDetail30": "Jogadores com assinaturas recorrentes de 1 mês ou assinaturas de Plano de Grupo receberão 2 Ampulhetas Místicas e 20 Gemas.",
|
||||
"subscriptionDetail31": "Jogadores com assinaturas recorrentes de 3 ou 6 meses receberão 4 Ampulhetas Místicas e 20 Gemas.",
|
||||
"subscriptionDetail45": "Comprar assinaturas extras de presente me dará mais Ampulhetas Místicas ou um limite maior de Gemas mais rápido?",
|
||||
"subscriptionDetail451": "Cada assinatura presenteada aumentará a quantidade de meses em que o jogador terá benefícios de assinatura, permitindo que ele continue recebendo mais Ampulhetas Místicas e aumentos em seu limite de Gemas a cada mês que passa.",
|
||||
"subscriptionDetail110": "Se você aumentar a quantidade de Gemas que pode comprar a cada mês e cancelar sua assinatura, poderá recuperá-la na mesma quantidade a qualquer momento no futuro, mesmo se comprar um nível de assinatura menor.",
|
||||
"subscriptionDetail21": "Os quatro níveis de assinatura eram conhecidos por causar complicações ao fazer upgrade ou downgrade para níveis diferentes.",
|
||||
"subscriptionDetail33": "Para receber essas recompensas, sua conta deve ter uma assinatura recorrente ativa antes de 19 de novembro.",
|
||||
"subscriptionDetail4400": "Se você atualmente desbloqueou <%= initialNumber %> Gemas por mês, você será definido como <%= roundedNumber %>.",
|
||||
"subscriptionDetail450": "Como as Ampulhetas Místicas e o aumento do limite de Gemas agora são benefícios mensais, comprar várias assinaturas de presente não dará mais benefícios de uma só vez.",
|
||||
"subscriptionDetail24": "Queríamos que os assinantes tivessem mais de quatro chances por ano para coletar itens da Loja dos Viajantes do Tempo.",
|
||||
"subscriptionDetail25": "Entendemos que as finanças mudam e não queríamos punir os assinantes, retirando benefícios que eles haviam conquistado, por assinaturas canceladas.",
|
||||
"subscriptionHeading3": "Recompensas do dia de Lançamento",
|
||||
"subscriptionPara1": "Para facilitar a transição para o novo formato, os assinantes atuais podem esperar alguns brindes extras no dia do lançamento. Gostaríamos de agradecer sinceramente pelo seu apoio contínuo durante essa mudança!",
|
||||
"subscriptionDetail32": "Jogadores com assinaturas recorrentes de 12 meses receberão o bônus de 12 Ampulhetas Místicas mencionado acima e 20 Gemas.",
|
||||
"subscriptionDetail40": "Sou assinante. Quando receberei minha primeira Ampulheta Mística e capacidade de Gemas no novo formato?",
|
||||
"subscriptionDetail400": "Assinantes atuais receberão sua primeira Ampulheta Mística e +2 Gemas adicionadas a capacidade mensal no primeiro login do mês seguinte ao lançamento. Isso significa que, se você já tiver feito login em novembro, seu primeiro aumento regular ocorrerá em dezembro.",
|
||||
"subscriptionDetail41": "O preço das assinaturas mudará no lançamento?",
|
||||
"subscriptionDetail410": "Essas mudanças não afetarão o valor das assinaturas.",
|
||||
"subscriptionDetail42": "Se eu não fizer login por um mês enquanto for assinante, perderei esses benefícios?",
|
||||
"subscriptionDetail420": "Assim como nos Conjuntos de Equipamentos Misteriosos, você não perderá nenhum aumento de Ampulhetas Místicas ou de limite de Gemas se não fizer login enquanto estiver inscrito. Na próxima vez que fizer login, você receberá todos os benefícios devidos para cada mês em que esteve inscrito.",
|
||||
"subscriptionDetail43": "Se eu assinar uma assinatura recorrente e depois cancelar, ainda receberei benefícios?",
|
||||
"subscriptionDetail430": "O cancelamento de uma assinatura recorrente definirá uma data de término para seus benefícios, mas você ainda terá acesso total a todas as vantagens da assinatura antes dessa data. Isso significa que você ainda receberá Ampulhetas Místicas mensais e aumentos no limite de Gemas no início de cada mês em que tiver acesso a esses benefícios.",
|
||||
"subscriptionDetail44": "Já sou assinante. Quantas Gemas terei disponíveis no Mercado a cada mês após a mudança?",
|
||||
"subscriptionDetail440": "No dia em que essas mudanças entrarem em vigor, os assinantes atuais com um número ímpar de Gemas por mês verão estes ajustes em seu limite de Gemas:",
|
||||
"subscriptionDetail46": "Se eu já tive uma assinatura, posso desbloquear meu antigo limite de gemas se eu assinar novamente agora?",
|
||||
"subscriptionDetail460": "Como costumávamos redefinir a quantidade de Gemas que você podia comprar a cada mês quando seus benefícios acabavam, jogadores com benefícios de assinatura expirados terão que começar do zero com este novo sistema.",
|
||||
"subscriptionDetail470": "Os benefícios para assinantes do Plano de Grupo serão os mesmos de uma assinatura recorrente de 1 mês. Você receberá uma Ampulheta Mística no início de cada mês e a quantidade de Gemas que você pode comprar mensalmente no Mercado aumentará em 2 até atingir 50.",
|
||||
"subscriptionDetail48": "Haverá alguma mudança em outros benefícios da assinatura, como os Conjuntos de Equipamento Misteriosos?",
|
||||
"subscriptionDetail480": "Essas mudanças afetam apenas Ampulhetas Místicas e Gemas de assinantes. Todos os outros benefícios permanecerão os mesmos.",
|
||||
"subscriptionPara2": "Caso tenha alguma dúvida que não esteja nas respostas acima, você pode entrar em contato com nossa equipe pelo e-mail <%= mailto %>.",
|
||||
"subscriptionPara3": "Esperamos que esse novo cronograma seja mais previsível, permita mais acesso ao incrível estoque de itens na Loja dos Viajantes do Tempo e dê ainda mais motivação para progredir em suas tarefas a cada mês!"
|
||||
}
|
||||
|
||||
@@ -22,34 +22,34 @@
|
||||
"guidanceForBlacksmiths": "Diretrizes para Ferreiros",
|
||||
"history": "História",
|
||||
"invalidEmail": "Um endereço de e-mail válido é necessário para redefinir sua senha.",
|
||||
"login": "Login",
|
||||
"login": "Iniciar sessão",
|
||||
"logout": "Desconectar",
|
||||
"marketing1Header": "Melhore Seus Hábitos Jogando",
|
||||
"marketing1Lead1Title": "Sua Vida, O Jogo de RPG",
|
||||
"marketing1Lead1": "Habitica é um jogo que te ajuda a melhorar hábitos da vida real. Ele \"gamifica\" sua vida ao tornar todas suas tarefas (Hábitos, Diárias e Afazeres) em pequenos monstros que você precisa derrotar. Quanto melhor for nisso, mais você avança no jogo. Se você deslizar na vida real, seu personagem começa a retroceder no jogo.",
|
||||
"marketing1Lead2Title": "Consiga Equipamentos Incríveis",
|
||||
"marketing1Lead2": "Melhore seus hábitos para desenvolver seu avatar. Mostre os belos equipamentos que você ganhou!",
|
||||
"marketing1Lead3Title": "Encontre Prêmios Aleatórios",
|
||||
"marketing1Lead3": "Para alguns, é a aposta que os motiva: um sistema chamado \"gratificação estocástica\". O Habitica acomoda todos os estilos de reforço e punição: positivo, negativo, previsível e aleatório.",
|
||||
"marketing2Header": "Dispute com seus amigos",
|
||||
"marketing2Lead1Title": "Produtividade Social",
|
||||
"marketing2Lead1": "Enquanto você pode jogar Habitica sozinho, as coisas ficam realmente interessantes quando você começa a colaborar, competir e ajudar uns aos outros. A parte mais efetiva de qualquer programa de auto-aperfeiçoamento é a cobrança social, e qual o melhor ambiente para responsabilidade e competição do que um jogo?",
|
||||
"marketing2Lead2Title": "Lute Contra Monstros",
|
||||
"marketing2Lead2": "O que é um RPG sem batalhas? Enfrente monstros junto com seu grupo. Chefões são um \"modo de super cobrança\", um dia que você falta à academia é um dia que o chefão machuca *todo mundo!*",
|
||||
"marketing1Header": "Melhore seus hábitos um nível por vez!",
|
||||
"marketing1Lead1Title": "Gamifique sua vida",
|
||||
"marketing1Lead1": "O Habitica é o aplicativo perfeito para quem tem dificuldades com listas de afazeres. Nós usamos mecânicas de jogo familiares, como recompensas em Ouro, Experiência e itens, para ajudar você a se sentir produtivo e aumentar sua sensação de conquista ao completar tarefas. Quanto melhor você for nas suas tarefas, mais você progride dentro do jogo.",
|
||||
"marketing1Lead2Title": "Equipe-se com estilo",
|
||||
"marketing1Lead2": "Colete espadas, armaduras e muito mais com o Ouro que você ganha ao concluir tarefas. Com centenas de peças para coletar e escolher, você nunca vai ficar sem combinações para experimentar. Otimize para atributos, estilo, ou os dois! ",
|
||||
"marketing1Lead3Title": "Seja recompensado pelo seu esforço",
|
||||
"marketing1Lead3": "Ter algo pelo que esperar pode ser a diferença entre concluir uma tarefa ou tê-la provocando você por semanas. Quando a vida não oferece uma recompensa, o Habitica cuida disso! Você será recompensado por cada tarefa, mas surpresas o aguardam a cada esquina — então continue progredindo! ",
|
||||
"marketing2Header": "Junte-se aos amigos",
|
||||
"marketing2Lead1Title": "Produtividade social",
|
||||
"marketing2Lead1": "Aumente sua motivação colaborando, competindo e interagindo com outras pessoas! O Habitica foi criado para aproveitar a parte mais eficaz de qualquer programa de autoaperfeiçoamento: a responsabilidade social.",
|
||||
"marketing2Lead2Title": "Derrote monstros nas missões",
|
||||
"marketing2Lead2": "Participe de uma das nossas centenas de missões com um Grupo de amigos para entrar na briga. Os monstros das missões levam sua responsabilidade ao limite. Esquecer de usar fio dental significa dano a todos!",
|
||||
"marketing2Lead3Title": "Desafiem-se",
|
||||
"marketing2Lead3": "Desafios lhe permite competir com amigos e desconhecidos. Quem se sair melhor ao final do desafio ganha prêmios especiais.",
|
||||
"marketing3Header": "Apps e Extensões",
|
||||
"marketing3Lead1": "Os aplicativos para **iPhone e Android** permitem que você cuide de suas tarefas em qualquer lugar. Nós sabemos que conectar no website para clicar em botões pode ser chato.",
|
||||
"marketing3Lead2Title": "Integrações",
|
||||
"marketing3Lead2": "Outras **Ferramentas de Terceiros** conectam o Habitica a vários outros aspectos da sua vida. Nossa API fornece uma integração fácil a ferramentas como a [Extensão do Chrome](https://chrome.google.com/webstore/detail/habitica/pidkmpibnnnhneohdgjclfdjpijggmjj?hl=en-US), pela qual você perde pontos ao navegar em sites improdutivos e ganha pontos nos sites produtivos. [Veja mais informações aqui](https://habitica.fandom.com/pt-br/wiki/Extens%C3%B5es,_Add-ons_e_Personaliza%C3%A7%C3%B5es).",
|
||||
"marketing4Header": "Uso Organizacional",
|
||||
"marketing4Lead1": "Educação é um dos melhores setores para gamificação. Todos nós sabemos o quanto estudantes estão grudados no telefone e em jogos hoje em dia - aproveite esse poder! Coloque seus estudantes uns contra os outros em uma competição amigável. Recompense bons comportamentos com prêmios raros. Veja suas notas e comportamentos melhorarem.",
|
||||
"marketing4Lead1Title": "Gamificação na Educação",
|
||||
"marketing4Lead2": "Os custos de assistência médica estão subindo, e alguém tem que ceder. Centenas de programas são feitos para reduzir custos e melhorar o bem-estar. Acreditamos que o Habitica pode construir um caminho muito importante em direção a estilos de vida saudáveis.",
|
||||
"marketing4Lead2Title": "Gamificação em Saúde e Bem-estar",
|
||||
"marketing4Lead3-1": "Quer gamificar sua vida?",
|
||||
"marketing2Lead3": "Participe dos desafios criados pela nossa comunidade para obter listas de tarefas personalizadas que se adaptam aos seus interesses e objetivos. Dê o seu melhor para concorrer ao prêmio de Gemas concedido ao vencedor!",
|
||||
"marketing3Header": "Outras maneiras de usar o Habitica",
|
||||
"marketing3Lead1": "Você pode instalar o Habitica no seu dispositivo Android ou iOS para concluir tarefas em qualquer lugar. Confira nossos aplicativos premiados para uma abordagem inovadora para realizar tarefas.",
|
||||
"marketing3Lead2Title": "Comunidade de Código Aberto",
|
||||
"marketing3Lead2": "Temos orgulho de ser um projeto de código aberto que aceita contribuições da nossa comunidade dedicada. Adapte o Habitica às suas necessidades ou contribua para melhorar a experiência de todos os jogadores ao redor do mundo. Visite-nos no [GitHub](https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica) para saber mais!",
|
||||
"marketing4Header": "Além das tarefas domésticas",
|
||||
"marketing4Lead1": "A educação é um dos melhores lugares para um pouco de gamificação! Quebre a monotonia das aulas cotidianas adicionando um pouco de jogos. O Habitica pode ser uma maneira divertida de acompanhar as tarefas de casa, criar desafios em sala de aula e permitir que seus alunos exibam suas conquistas.",
|
||||
"marketing4Lead1Title": "Gamificação na educação",
|
||||
"marketing4Lead2": "Construir um estilo de vida mais saudável pode facilmente se tornar uma tarefa árdua. O Habitica ajuda você a monitorar todos os aspectos dos seus objetivos de condicionamento físico com horários flexíveis e intensidade que se adaptam a você onde você estiver. Então, divirta-se enquanto busca uma saúde melhor!",
|
||||
"marketing4Lead2Title": "Gamificação em saúde e bem-estar",
|
||||
"marketing4Lead3-1": "Pronto para se divertir realizando tarefas?",
|
||||
"marketing4Lead3-2": "Interessado em coordenar um grupo em educação, bem-estar e outros?",
|
||||
"marketing4Lead3Title": "Gamifique Tudo",
|
||||
"marketing4Lead3Title": "Comece sua jornada!",
|
||||
"mobileAndroid": "Aplicativo Android",
|
||||
"mobileIOS": "Aplicativo iOS",
|
||||
"oldNews": "Notícias",
|
||||
@@ -86,14 +86,14 @@
|
||||
"sync": "Sincronizar",
|
||||
"tasks": "Tarefas",
|
||||
"teams": "Times",
|
||||
"terms": "Termos e Condições",
|
||||
"terms": "Termos de Serviço",
|
||||
"tumblr": "Tumblr",
|
||||
"localStorageTryFirst": "Se você estiver tendo problemas com o Habitica, clique no botão abaixo para limpar o armazenamento local e a maioria dos cookies deste website (outros websites não serão afetados). Você precisará entrar na sua conta novamente depois de fazer isso, então fique à vontade para saber dos detalhes de log-in, que podem ser encontrados em Configurações-> <%= linkStart %>Site<%= linkEnd %>.",
|
||||
"localStorageTryNext": "Se os problemas persistirem, por favor <%= linkStart %>Relate um Bug<%= linkEnd %> se ainda não o fez.",
|
||||
"localStorageClear": "Limpar Dados",
|
||||
"localStorageClearExplanation": "Este botão irá limpar o armazenamento local e a maioria dos cookies, e irá desconecta-lo.",
|
||||
"username": "Nome de Usuário",
|
||||
"emailOrUsername": "E-mail ou nome de usuário (diferencia minúsculas de maiúsculas)",
|
||||
"emailOrUsername": "Nome de usuário ou E-mail (diferencia minúsculas de maiúsculas)",
|
||||
"work": "Trabalho",
|
||||
"reportAccountProblems": "Reportar Problemas na Conta",
|
||||
"reportCommunityIssues": "Reportar Problemas com a Comunidade",
|
||||
@@ -101,7 +101,7 @@
|
||||
"generalQuestionsSite": "Perguntas Gerais sobre o Site",
|
||||
"businessInquiries": "Consultas de Negócios e Marketing",
|
||||
"merchandiseInquiries": "Consultas sobre Mercadorias Físicas (Camisetas, Adesivos)",
|
||||
"tweet": "Tweet",
|
||||
"tweet": "Tuíte",
|
||||
"checkOutMobileApps": "Confira nossos aplicativos móveis!",
|
||||
"missingAuthHeaders": "Faltando cabeçalhos de autenticação.",
|
||||
"missingUsernameEmail": "Faltando nome de usuário ou e-mail.",
|
||||
@@ -122,7 +122,7 @@
|
||||
"passwordConfirmationMatch": "A confirmação de senha não corresponde à senha.",
|
||||
"passwordResetPage": "Mudar a Senha",
|
||||
"passwordReset": "Se nós tivermos seu e-mail ou nome de usuário nos nossos arquivos, as instruções para mudar sua senha já foram mandadas para o seu e-mail.",
|
||||
"invalidLoginCredentialsLong": "Oh não - seu endereço de e-mail / nome de usuário ou senha está incorreto.\n- Certifique-se de que foram digitados corretamente. Seu nome de usuário e senha diferenciam minusculas de maiúsculas.\n- Você pode ter se cadastrado com o Facebook ou Google, não com o e-mail. Cheque tentando fazer login com estas opções.\n- Se você esqueceu sua senha, clique em \"Esqueci a Senha\".",
|
||||
"invalidLoginCredentialsLong": "Seu e-mail, nome de usuário ou senha estão incorretos. Tente novamente ou selecione \"Esqueceu sua senha?\"",
|
||||
"invalidCredentials": "Não há uma conta associada a esses dados.",
|
||||
"accountSuspended": "Essa conta, (ID de Usuário: \"<%= userId %>\") foi bloqueada por violar as Diretrizes da Comunidade (https://habitica.com/static/community-guidelines) ou os Termos de Serviço (https://habitica.com/static/terms). Para detalhes ou solicitar o desbloqueio, por favor, entre em contato com nosso Administrador de Comunidade através do e-mail <%= communityManagerEmail %> ou peça para seu pais ou tutores para enviar o e-mail. Por gentileza, não se esqueça de colocar no conteúdo do e-mail o seu @NomeDeUsuário.",
|
||||
"accountSuspendedTitle": "Conta suspensa",
|
||||
@@ -132,12 +132,12 @@
|
||||
"invalidReqParams": "Parâmetros de requerimento inválidos.",
|
||||
"memberIdRequired": "\"member\" precisa ser um UUID válido.",
|
||||
"heroIdRequired": "\"heroId\" precisa ser um UUID válido.",
|
||||
"cannotFulfillReq": "Sua solicitação não pode ser cumprida. Mande um e-mail para admin@habitica.com se esse erro persistir.",
|
||||
"cannotFulfillReq": "Insira um endereço de e-mail válido. Envie um e-mail para admin@habitica.com se o erro persistir.",
|
||||
"modelNotFound": "Este modelo não existe.",
|
||||
"signUpWithSocial": "Cadastre-se com <%= social %>",
|
||||
"signUpWithSocial": "Continue com <%= social %>",
|
||||
"loginWithSocial": "Entre com <%= social %>",
|
||||
"confirmPassword": "Confirmar Senha",
|
||||
"usernameLimitations": "O nome de usuário deve conter entre 1 e 20 caracteres; dentre eles, apenas letras de A a Z, números de 0 a 9, hifens ou underlines, não podendo ser incluso quaisquer termos inapropriados.",
|
||||
"usernameLimitations": "Os nomes de usuário podem ser alterados a qualquer momento. Eles devem ter de 1 a 20 caracteres, contendo apenas letras de A a Z, números de 0 a 9, hífens ou sublinhados.",
|
||||
"usernamePlaceholder": "Ex: HabitIcante",
|
||||
"emailPlaceholder": "Ex: habiticante@exemplo.com",
|
||||
"passwordPlaceholder": "Ex: ******************",
|
||||
@@ -179,5 +179,13 @@
|
||||
"socialAlreadyExists": "Este login já está vinculado a uma conta existente do Habitica.",
|
||||
"footerProduct": "Produto",
|
||||
"translateHabitica": "Traduza o Habitica",
|
||||
"incorrectResetPhrase": "Por favor, escreva <%= magicWord %> em letras maiúsculas para redefinir sua conta."
|
||||
"incorrectResetPhrase": "Por favor, escreva <%= magicWord %> em letras maiúsculas para redefinir sua conta.",
|
||||
"minPasswordLengthLogin": "Sua senha possui pelo menos 8 caracteres.",
|
||||
"enterValidEmail": "Por favor, insira um endereço de email válido.",
|
||||
"emailBlockedRegistration": "Este E-mail está bloqueado para cadastro. Se você acha que isso é um engano, entre em contato conosco pelo admin@habitica.com.",
|
||||
"whatToCallYou": "Como devemos te chamar?",
|
||||
"acceptPrivacyTOS": "Você confirma que tem pelo menos 18 anos de idade e que leu e concorda com nossos <a href='/static/terms' target='_blank'>Termos de Serviço</a> e <a href='/static/privacy' target='_blank'>Política de Privacidade</a>",
|
||||
"marketing3Lead1Title": "Aplicativos Android e iOS",
|
||||
"marketing4Lead3Button": "Comece hoje mesmo",
|
||||
"missingClientHeader": "Cabeçalhos x-client ausentes."
|
||||
}
|
||||
|
||||
@@ -3084,7 +3084,7 @@
|
||||
"armorArmoireDragonKnightsArmorNotes": "Canalize a força e o poder de um dragão com esta armadura feita de prata e escamas desprendidas. Aumenta a Força em <%= str %>. Armário Encantado: Conjunto do Cavaleiro-Dragão (Item 2 de 3)",
|
||||
"armorArmoireDragonKnightsArmorText": "Armadura do Cavaleiro-Dragão",
|
||||
"weaponArmoireDragonKnightsLanceText": "Lança do Cavaleiro-Dragão",
|
||||
"weaponArmoireDragonKnightsLanceNotes": "Esta lança vermelha e prateada derrubou muitos oponentes de suas montarias. Aumenta a Constituição em <%= con %>. Armário Encantado: Conjunto do Cavaleiro-Dragão (Item 3 de 3)",
|
||||
"weaponArmoireDragonKnightsLanceNotes": "Esta lança vermelha e prateada derrubou muitos oponentes de suas montarias. Aumenta a Constituição em <%= con %>. Armário Encantado: Conjunto do Cavaleiro-Dragão (Item 3 de 3).",
|
||||
"headArmoireDragonKnightsHelmText": "Elmo do Cavaleiro-Dragão",
|
||||
"armorMystery202502Text": "Traje de Amável Arlequim",
|
||||
"headMystery202502Text": "Chapéu de Amável Arlequim",
|
||||
@@ -3101,7 +3101,7 @@
|
||||
"weaponSpecialSpring2025RogueNotes": "Com um golpe, você pode destruir qualquer obstáculo no caminho das suas metas. Aumenta Força em <%= str %>. Equipamento de Edição Limitada da primavera de 2025.",
|
||||
"weaponSpecialSpring2025MageText": "Cajado de Louva-a-deus",
|
||||
"armorSpecialWinter2025MageText": "Capa da Aurora",
|
||||
"weaponArmoireSpookyCandyBucketNotes": "Com uma fantasia dessas, você vai pegar muito doce! Sorte que você tem esse balde infinito para guardar todos eles. Tente não belisca-los até chegar em casa. Aumenta Inteligência em <%=int%>.Armário Encantado Conjunto Noite dos Sustos (Item 2 de 2)",
|
||||
"weaponArmoireSpookyCandyBucketNotes": "Com uma fantasia dessas, você vai pegar muito doce! Sorte que você tem esse balde infinito para guardar todos eles. Tente não belisca-los até chegar em casa. Aumenta Inteligência em <%=int%>.Armário Encantado Conjunto Noite dos Sustos (Item 2 de 2).",
|
||||
"armorSpecialWinter2025RogueText": "Fantasia de Neve",
|
||||
"armorSpecialSummer2024MageText": "Cauda de Anêmona",
|
||||
"weaponArmoireSpookyCandyBucketText": "Balde de Doces Assustador",
|
||||
@@ -3119,5 +3119,44 @@
|
||||
"weaponSpecialSpring2025RogueText": "Mangual de Ponta de Cristal",
|
||||
"weaponSpecialSpring2025HealerNotes": "Com uma onda, você pode invocar polinizadores para ajudá-lo em suas aventuras. Aumenta a Inteligência em <%= int %>. Equipamento de Edição Limitada da Primavera de 2025.",
|
||||
"weaponSpecialSpring2025MageNotes": "Com um único golpe, você pode usar magia elemental para controlar o ambiente ao seu redor. Aproveite e avance! Aumenta a Inteligência em <%= int %> e a Percepção em <%= per %>. Edição Limitada Equipamentos da primavera de 2025",
|
||||
"weaponSpecialSummer2025WarriorText": "Lança de vieira"
|
||||
"weaponSpecialSummer2025WarriorText": "Lança de vieira",
|
||||
"armorSpecialSpring2024MageNotes": "Essas pétalas bonitinhas vão te ajudar a mostrar seu poder em estilo. Aumenta Inteligência em <%=int%>. Equipamento de Edição Limitada Primavera 2024.",
|
||||
"weaponSpecialSpring2025HealerText": "Cajado de Flor Pluma",
|
||||
"weaponArmoireGildedKnightsSpearNotes": "Com esta arma, você pode garantir que todos sempre paguem suas dívidas. Aumenta a Força em <%= str %>. Armário Encantado: Conjunto do Cavaleiro Dourado (Item 3 de 3).",
|
||||
"weaponArmoireGildedKnightsSpearText": "Lança de Cavaleiro Dourado",
|
||||
"weaponSpecialSummer2025RogueText": "Tentáculo de Lula",
|
||||
"weaponSpecialSummer2025RogueNotes": "Este tentáculo se agarrará firmemente aos seus objetivos para que você não perca o ritmo ao concluir tarefas. Aumenta a Força em <%= str %>. Equipamento de Edição Limitada do Verão de 2025.",
|
||||
"weaponSpecialSummer2025HealerText": "Remo de Asa de Anjo Marinho",
|
||||
"weaponSpecialSummer2025HealerNotes": "Desenhe um oito conforme avança, progredindo bastante em suas tarefas. Aumenta a Inteligência em <%= int %>. Equipamento de Edição Limitada Verão 2025.",
|
||||
"weaponSpecialSummer2025MageText": "Ramo Coral",
|
||||
"weaponSpecialSummer2025MageNotes": "Expanda seus talentos e habilidades para realizar uma variedade de tarefas. Aumenta a Inteligência em <%= int %> e a Percepção em <%= per %>. Equipamento de Edição Limitada Verão 2025.",
|
||||
"armorSpecialSummer2024WarriorNotes": "Depois de se transformar em um verdadeiro Guerreiro Tubarão-Baleia, nade corajosamente em direção às suas tarefas! Aumenta a Constituição em <%= con %>. Equipamento de Edição Limitada Verão 2024.",
|
||||
"weaponArmoireFunnyFoolBatonText": "Bastão de Bobo Engraçado",
|
||||
"weaponSpecialFall2025WarriorText": "Machado de Pé Grande",
|
||||
"weaponSpecialFall2025RogueText": "Espada Esqueleto",
|
||||
"weaponSpecialFall2025RogueNotes": "Uma arma poderosa para abrir caminho com segurança por uma floresta outonal repleta de obstáculos. Aumenta a Força em <%= str %>. Equipamento de Edição Limitada Outono 2025.",
|
||||
"weaponSpecialFall2025HealerText": "Machado Kobold",
|
||||
"weaponSpecialFall2025MageText": "Machado do Fantasma Mascarado",
|
||||
"weaponArmoireFunnyFoolBatonNotes": "Com um aceno de bastão, você pode dar uma piada, redirecionar a atenção ou atrair aplausos. Aumenta a Constituição e a Força em <%= attrs %> cada. Armário Encantado: Conjunto do Tolo Engraçado (Item 3 de 3).",
|
||||
"weaponArmoireStormKnightAxeNotes": "Reúna sua fúria e desfira um golpe como um trovão! Aumenta a Força em <%= str %>. Armário Encantado: Conjunto do Cavaleiro da Tempestade (Item 3 de 3).",
|
||||
"weaponArmoireBeekeepersSmokerNotes": "Use isto para acalmar suas abelhas e recuperar um pouco de mel. As abelhas não vão se importar. Sinceramente, todos nós precisamos de alguns minutos extras de calma de vez em quando. Aumenta a Inteligência em <%= int %>. Armário Encantado: Conjunto de Apicultor (Item 3 de 4).",
|
||||
"weaponArmoireBlacksmithsHammerText": "Martelo de Ferreiro",
|
||||
"weaponArmoireBlacksmithsHammerNotes": "Este martelo é para metalurgia, mas também é perfeitamente adequado para brasas e tarefas diárias em brasa. Aumenta a Força em <%= str %>. Armário Encantado: Conjunto de Ferreiro (Item 3 de 3).",
|
||||
"armorSpecialSpring2024RogueNotes": "Este manto rústico te protege mesmo com a mudança das estações. Aumenta a Percepção em <%= por %>. Equipamento de Edição Limitada Primavera 2024.",
|
||||
"armorSpecialSpring2024WarriorNotes": "Esta armadura de pedra estabilizadora ajudará você a se firmar enquanto ofusca tudo o que encontrar pela frente. Aumenta a Constituição em <%= con %>. Equipamento de Edição Limitada Primavera 2024.",
|
||||
"armorSpecialSummer2024WarriorText": "Cauda de Tubarão-Baleia",
|
||||
"weaponSpecialSummer2025WarriorNotes": "Não há como dizer a idade, mas ele permanecerá com você em muitas tarefas difíceis. Aumenta a Força em <%= str %>. Equipamento de Edição Limitada do Verão de 2025.",
|
||||
"weaponSpecialFall2025WarriorNotes": "Uma arma poderosa para abrir caminho com segurança por uma floresta outonal cheia de complicações. Aumenta a Força em <%= str %>. Equipamento de Edição Limitada Outono 2025.",
|
||||
"weaponSpecialFall2025HealerNotes": "Uma arma poderosa para abrir caminho com segurança por uma floresta outonal repleta de obstáculos. Aumenta a Inteligência em <%= int %>. Equipamento de Edição Limitada Outono 2025.",
|
||||
"weaponSpecialFall2025MageNotes": "Uma arma poderosa para abrir caminho em segurança por uma floresta outonal cheia de sustos. Aumenta a Inteligência em <%= int %> e a Percepção em <%= per %>. Equipamento de Edição Limitada Outono 2025.",
|
||||
"weaponMystery202511Text": "Espada Congelada",
|
||||
"weaponMystery202511Notes": "O brilho gélido desta espada realizará rapidamente até mesmo tarefas vermelho-escuras. Não concede nenhum benefício. Item de assinante de novembro 2025.",
|
||||
"weaponArmoireCorsairsBladeNotes": "Quer você use esta lâmina poderosa para saques ou proteção, fique feliz por tê-la trazido de terra firme. Apenas certifique-se de guardá-la em segurança quando não estiver em uso. Aumenta a Força em <%= str %>. Armário Encantado: Conjunto Corsário (Item 3 de 3).",
|
||||
"armorSpecialSpring2024WarriorText": "Armadura de Fluorita",
|
||||
"armorSpecialSpring2024HealerNotes": "Estas penas fabulosas ajudarão você a realizar seus sonhos mais felizes. Aumenta a Constituição em <%= con %>. Equipamento de Edição Limitada Primavera 2024.",
|
||||
"weaponMystery202508Text": "Lâmina Carmesim Brilhante",
|
||||
"weaponMystery202508Notes": "Esta lâmina giratória aterrorizará qualquer monstro ou item diário vermelho que cruzar seu caminho! Não concede nenhum benefício. Item de assinante de agosto 2025.",
|
||||
"weaponArmoireBeekeepersSmokerText": "Fumante",
|
||||
"armorSpecialSpring2024RogueText": "Manto Derrete Neves",
|
||||
"armorSpecialSpring2024MageText": "Vestes de Hibisco"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stable": "Estábulo",
|
||||
"stable": "Mascotes e Montarias",
|
||||
"pets": "Mascotes",
|
||||
"activePet": "Mascote ativo",
|
||||
"noActivePet": "Sem Mascote ativo",
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
"subscription": "Assinatura",
|
||||
"subscriptions": "Assinaturas",
|
||||
"sendGems": "Enviar Gemas",
|
||||
"buyGemsGold": "Comprar Gemas com Ouro",
|
||||
"buyGemsGold": "Gemas grátis mensais",
|
||||
"mustSubscribeToPurchaseGems": "É necessário ser assinante para comprar gemas com ouro",
|
||||
"reachedGoldToGemCapQuantity": "Seu pedido de <%= quantity %> Gemas ultrapassa o limite que você pode comprar para este mês (<%= convCap %>). O limite fica disponível nos três primeiros dias de cada mês. Obrigado por assinar!",
|
||||
"mysteryItem": "Itens mensais exclusivos",
|
||||
"mysteryItemText": "A cada mês você receberá um item cosmético único para o seu personagem! Além disso, para cada três meses de assinatura consecutivos, os Viajantes do Tempo Misteriosos te darão acesso a itens cosméticos do passado (e do futuro!).",
|
||||
"exclusiveJackalopePet": "Mascote exclusivo",
|
||||
"exclusiveJackalopePet": "Mascote especial",
|
||||
"giftSubscription": "Deseja presentear os benefícios de uma assinatura para outra pessoa?",
|
||||
"giftSubscriptionText4": "Obrigado por ajudar o Habitica!",
|
||||
"groupPlans": "Planos de Times",
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
"optOutOfClasses": "Отказаться",
|
||||
"chooseClass": "Выберите свой класс",
|
||||
"chooseClassLearnMarkdown": "[Узнать больше о системе классов в стране Habitica](https://habitica.fandom.com/ru/wiki/Система_классов)",
|
||||
"optOutOfClassesText": "Не хотите утруждать себя выбором класса? Или предпочитаете определиться позже? Отказавшись от класса, вы останетесь простым воином без специальных способностей. Вы также можете прочитать про систему классов на нашей вики позже и включить классы в любое время на странице Пользователь -> Настройки.",
|
||||
"optOutOfClassesText": "Пока не готовы выбрать? Это не к спеху. Если откажетесь, вы можете ознакомиться с каждым классом в <a href='/static/faq#what-classes' target='_blank'>нашем FAQ</a> и по готовности зайти в «Настройки» для выбора класса.",
|
||||
"selectClass": "Выбрать <%= heroClass %>",
|
||||
"select": "Выбрать",
|
||||
"stealth": "Хитрость",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"set": "Комплект",
|
||||
"set": "Набір",
|
||||
"equipmentType": "Тип",
|
||||
"klass": "Клас",
|
||||
"groupBy": "Групувати за <%= type %>",
|
||||
"classBonus": "(Цей предмет відповідає вашому класу, тому він отримує додатковий множник характеристик 1.5)",
|
||||
"classArmor": "Обладунки класу",
|
||||
"featuredset": "Рекомендований набір <%= name %>",
|
||||
"mysterySets": "Містичний комплект",
|
||||
"mysterySets": "Містичні Набори",
|
||||
"gearNotOwned": "Ви не володієте цим предметом.",
|
||||
"noGearItemsOfType": "Ви не володієте жодним з цих предметів.",
|
||||
"classLockedItem": "Цей предмет доступний лише для певного класу. На рівні 10 або вище ви можете змінити свій клас у Користувач > Налаштування > Розвиток персонажа!",
|
||||
"tierLockedItem": "Цей товар доступний лише після того, як ви купите попередні товари з послідовності. Продовжуйте працювати!",
|
||||
"tierLockedItem": "Цей предмет доступний лише після того, як ви купите попередні товари з послідовності. Продовжуйте працювати!",
|
||||
"sortByType": "Тип",
|
||||
"sortByPrice": "Ціна",
|
||||
"sortByCon": "ВИТ",
|
||||
@@ -2402,5 +2402,6 @@
|
||||
"eyewearMystery202308Text": "Заспані очі",
|
||||
"eyewearMystery202303Text": "Замріяні очі",
|
||||
"eyewearMystery202312Text": "Зимово-блакитні очі",
|
||||
"eyewearMystery202204AText": "Віртуальне обличчя"
|
||||
"eyewearMystery202204AText": "Віртуальне обличчя",
|
||||
"weaponSpecialSpring2024RogueText": "Срібне лезо"
|
||||
}
|
||||
|
||||
@@ -257,5 +257,30 @@
|
||||
"spring2024HibiscusMageSet": "Набір гібіскуса (маг)",
|
||||
"spring2024BluebirdHealerSet": "Набір синьої пташки (цілитель)",
|
||||
"spring2024MeltingSnowRogueSet": "Набір танучого снігу (розбійник)",
|
||||
"spring2024FluoriteWarriorSet": "Флюоритовий набір (воїн)"
|
||||
"spring2024FluoriteWarriorSet": "Флюоритовий набір (воїн)",
|
||||
"summer2024WhaleSharkWarriorSet": "Набір Китової Акули (Воїн)",
|
||||
"summer2024NudibranchRogueSet": "Набір Голозябровий Молюск (Розбійник)",
|
||||
"summer2024SeaSnailHealerSet": "Набір Морський Равлик(Цілитель)",
|
||||
"summer2024SeaAnemoneMageSet": "Набір Морська Анемона(Маг)",
|
||||
"fall2025SasquatchWarriorSet": "Набір Воїна Снігова Людина",
|
||||
"winter2025MooseWarriorSet": "Набір Лосиний Воїн",
|
||||
"winter2025AuroraMageSet": "Набір Мага Аврора",
|
||||
"winter2025StringLightsHealerSet": "Набір Цілителя Вогні Гірлянди",
|
||||
"winter2025SnowRogueSet": "Набір Сніжний Розбійник",
|
||||
"spring2025SunshineWarriorSet": "Набір Воїна Сонячного Сяйва",
|
||||
"spring2025CrystalPointRogueSet": "Набір Кришталевий Розбійник",
|
||||
"spring2025PlumeriaHealerSet": "Набір Цілителя Плюмерія",
|
||||
"spring2025MantisMageSet": "Набір Мага Богомол",
|
||||
"fall2024SpaceInvaderHealerSet": "Набір Космічний Загарбник (Цілитель)",
|
||||
"fall2024BlackCatRogueSet": "Набір Чорний Кіт (Розбійник)",
|
||||
"fall2024UnderworldSorcerorMageSet": "Набір Підземний Чаклун (Маг)",
|
||||
"fall2024FieryImpWarriorSet": "Набір Вогняний Біс(Воїн)",
|
||||
"summer2025ScallopWarriorSet": "Набір Воїна Морський Гребінець",
|
||||
"summer2025SquidRogueSet": "Набір Розбійника Кальмар",
|
||||
"summer2025SeaAngelHealerSet": "Набір Цілителя Морський Ангел",
|
||||
"summer2025FairyWrasseMageSet": "Набір Мага Казкова Врассея",
|
||||
"fall2025SkeletonRogueSet": "Набір Розбійника Скелет",
|
||||
"fall2025KoboldHealerSet": "Набір Цілителя Кобольд",
|
||||
"fall2025MaskedGhostMageSet": "Набір Мага Замаскований Привид",
|
||||
"gemSaleLimitationsText": "Ця акція діє лише протягом обмеженого періоду часу. Ця подія розпочнеться <%= eventStartMonth %> <%= eventStartOrdinal %> о <%= eventStartTime %> <%= timeZone %> і закінчиться <%= eventEndMonth %> <%= eventEndOrdinal %> о <%= eventEndTime %> <%= timeZone %>. Акційна пропозиція діє лише при покупці Самоцвітів для себе."
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"questSpiderBoss": "Павук",
|
||||
"questSpiderDropSpiderEgg": "Яйце павука",
|
||||
"questSpiderUnlockText": "Розблоковує купівлю павучих яєць на ринку",
|
||||
"questGroupVice": "Вайс - Змій із тіней",
|
||||
"questGroupVice": "Вайс - Змій Тіней",
|
||||
"questVice1Text": "Вайс (частина 1): Звільніться від впливу дракона",
|
||||
"questVice1Notes": "Кажуть, що в печерах гори Габітика сховане жахливе зло. Чудовисько, присутність якого ламає волю сильних героїв цієї землі, навертаючи їх у бік шкідливих звичок та лінощів! Звір — це великий дракон неймовірної сили, що складається з самих тіней: Вайс, підступний Змій із тіней. Відважні мешканці, встаньте і переможете цю мерзенну тварюку раз і назавжди, але тільки якщо ви вірите, що зможете протистояти його величезній силі. <br><br>Як ви можете розраховувати на боротьбу зі звіром, якщо він уже контролює вас? Не ставайте жертвою ліні та пороку! Працюйте наполегливо, щоб боротися з темним впливом дракона і розвіяти його владу над вами!",
|
||||
"questVice1Boss": "Тінь Вайса",
|
||||
@@ -645,7 +645,7 @@
|
||||
"questRobotDropRobotEgg": "Яйце робота",
|
||||
"questRobotText": "Загадкові механічні дива!",
|
||||
"mythicalMarvelsText": "Набір квестів \"Фантастичні звірі\"",
|
||||
"mythicalMarvelsNotes": "Містить \"Переконати королеву єдинорогів\", \"Полум'яний ґрифон\" та \"Небезпека на глибині: Напад морського змія!\".",
|
||||
"mythicalMarvelsNotes": "Містить Квести щоб отримати яйця Єдинорога, Ґрифона, та Морського змія: \"Переконати королеву єдинорогів\", \"Полум'яний ґрифон\" та \"Небезпека на глибині: Напад морського змія!\".",
|
||||
"rockingReptilesText": "Набір квестів \"Викопні рептилії\"",
|
||||
"delightfulDinosText": "Набір квестів \"Вражаючі динозаври\"",
|
||||
"jungleBuddiesText": "Набір квестів \"Друзі з джунглів\"",
|
||||
|
||||
@@ -797,5 +797,32 @@
|
||||
"backgroundColorfulCoralText": "多彩的珊瑚",
|
||||
"backgroundColorfulCoralNotes": "在彩色珊瑚間潛水。",
|
||||
"backgroundBoardwalkIntoSunsetText": "夕陽下的木板路",
|
||||
"backgroundBoardwalkIntoSunsetNotes": "在夕陽下的木板路上漫步。"
|
||||
"backgroundBoardwalkIntoSunsetNotes": "在夕陽下的木板路上漫步。",
|
||||
"backgroundMovingDayNotes": "為搬家日收拾行李。",
|
||||
"backgroundCoveredBridgeInAutumnText": "秋日廊橋",
|
||||
"backgroundCoveredBridgeInAutumnNotes": "穿過秋日的廊橋。",
|
||||
"backgroundBaobabForestText": "猴麵包樹森林",
|
||||
"backgroundBaobabForestNotes": "驚嘆地凝望猴麵包樹森林。",
|
||||
"backgrounds102023": "第113組:2023年10月發行",
|
||||
"backgrounds112023": "第114組:2023年11月發行",
|
||||
"backgroundSpectralCandleRoomText": "幽靈燭光房",
|
||||
"backgroundSpectralCandleRoomNotes": "在幽靈燭光房裡與靈魂交流。",
|
||||
"backgroundMonstrousCaveText": "巨獸洞穴",
|
||||
"backgroundMonstrousCaveNotes": "凝視巨獸洞穴的深邃入口。",
|
||||
"backgroundJackOLanternStacksText": "南瓜燈堆",
|
||||
"backgroundJackOLanternStacksNotes": "欣賞滿地的南瓜燈堆。",
|
||||
"backgroundGiantCatText": "巨貓",
|
||||
"backgroundGiantCatNotes": "和巨貓一同小憩。",
|
||||
"backgroundBarrelCellarText": "酒桶地窖",
|
||||
"backgroundBarrelCellarNotes": "在酒桶地窖裡尋找美味佳餚。",
|
||||
"backgroundAutumnTreeTunnelText": "秋日樹林隧道",
|
||||
"backgroundAutumnTreeTunnelNotes": "欣賞秋日樹林隧道的美景。",
|
||||
"backgrounds122023": "第115組:2023年12月發行",
|
||||
"backgroundHolidayTreeForestNotes": "在森林裡裝飾節慶樹。",
|
||||
"backgroundHolidayTreeForestText": "節慶森林",
|
||||
"backgroundIceSculptureFestivalText": "冰雕節",
|
||||
"backgroundIceSculptureFestivalNotes": "參觀冰雕節。",
|
||||
"backgroundWinterFullMoonText": "冬日滿月",
|
||||
"backgroundWinterFullMoonNotes": "凝望冬日滿月。",
|
||||
"backgrounds012024": "SetSet第116組:2024年1月發行"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"webFaqStillNeedHelp": "如果您有任何疑問,但沒出現在以上列表中或是 [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ)中,請至 [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)裡詢問,我們相當樂意協助。",
|
||||
"commonQuestions": "常見問題",
|
||||
"faqQuestion25": "這些不同的任務類型是什麼?",
|
||||
"webFaqAnswer25": "習慣可以用於你希望每天做多次的事情,或是一些不確定的計劃。 它們可以被選擇完成或完不成,選擇完成會帶來金幣和經驗的獎勵,而另一種則會對你造成生命值傷害。\n\n每日任務是你希望更嚴謹地按計畫進行的重複任務,例如每天一次、每週三次或是每月四次。 錯過完成每日任務會導致你受到生命值傷害,但這些任務越艱難,帶來的獎勵就越豐厚!\n\n待辦事項是一次性任務,當你完成時會提供獎勵。 待辦事項可以設定截止時間,但你不會因為錯過時間而失去生命值。\n\n選擇最適合你想要完成的任務類型吧!",
|
||||
"webFaqAnswer25": "Habitica 使用三種不同的任務類型來滿足您的需求,分別是:習慣 (Habits)、每日任務 (Dailies),以及待辦事項 (To-Do's)。\n\n習慣 (Habits)\n「習慣」分為正面與負面兩種,適合用來追蹤那些您想在一天內多次執行、或沒有固定時程的行為。正面的習慣會為您帶來獎勵(如金幣和經驗值),而負面的習慣則會讓您失去生命值。\n\n每日任務 (Dailies)\n「每日任務」是您想要在更有結構的時程上完成的重複性任務。例如,每天一次、每週三次,或是一個月四次。若未完成每日任務會導致您失去生命值,但任務的難度越高,完成後的獎勵也越豐厚!\n\n待辦事項 (To-Do’s)\n「待辦事項」是一次性的任務,在您完成後會提供獎勵。待辦事項可以設定截止日期,但即使錯過了,您也不會因此失去生命值。\n\n請挑選最適合您想達成目標的任務類型吧!",
|
||||
"faqQuestion26": "什麼是範例任務?",
|
||||
"contentAnswer62": "情人節魔術孵化藥水現在已納入每月的時間表中。",
|
||||
"contentFaqPara3": "如果您有任何上述答案未涵蓋的問題,您可以隨時透過 <%= mailto %> 聯絡我們的團隊!我們對於新內容發行計畫感到非常興奮,並期待未來有更多的計畫能幫助所有玩家讓 Habitica 變得更好。",
|
||||
@@ -29,7 +29,7 @@
|
||||
"subscriptionHeading2": "我們為什麼要做這些改變?",
|
||||
"subscriptionDetail20": "在目前的架構下,您很難了解您會收到多少個神秘沙漏,以及何時會收到。",
|
||||
"faqQuestion27": "為什麼任務會改變顏色?",
|
||||
"faqQuestion28": "如果我需要休息,可以暫停每日任務嗎?",
|
||||
"faqQuestion28": "如果我需要休息一下,可以暫停我的每日任務嗎?",
|
||||
"faqQuestion29": "如何恢復 HP(生命值)?",
|
||||
"faqQuestion30": "HP(生命值)歸零時會發生什麼事?",
|
||||
"faqQuestion32": "要怎麼選擇職業?",
|
||||
@@ -64,8 +64,8 @@
|
||||
"sunsetFaqHeader12": "公會銀行寶石會如何?",
|
||||
"sunsetFaqPara21": "公會銀行中的寶石將在 8 月 8 日公會服務結束時退還給公會領袖。",
|
||||
"sunsetFaqHeader2": "為什麼酒館和公會服務要結束?",
|
||||
"webFaqAnswer26": "正向習慣(你想要培養的行為;應該有「+」按鈕)\n\n * 吃維他命\n * 使用牙線\n * 學習一小時\n\n負向習慣(你想要限制或避免的行為;應該有「-」按鈕)\n\n * 抽菸\n * 無止盡滑手機\n * 咬指甲\n\n雙向習慣(同時包含正面與負面選項的行為;應有「+」與「-」按鈕)\n\n * 喝水 vs. 喝汽水\n * 學習 vs. 拖延\n\n每日任務範例(你想定期執行的事情)\n * 洗碗\n * 澆花\n * 30 分鐘的運動\n\n待辦事項範例(只需要完成一次的事情)\n\n * 預約時間\n * 整理衣櫃\n * 完成報告",
|
||||
"webFaqAnswer27": "任務的顏色能夠一目了然地看出其價值。所有任務的初始顏色都是黃色(代表中性)、藍色(代表較好)、紅色(代表較差)。以下是決定每種任務類型價值的方式:\n\n習慣的顏色會根據你是按下「+」或「-」按鈕而變藍或變紅。如果沒完成某個正向習慣和負向習慣,久了它們就會逐漸變成黃色;雙向習慣的顏色則只會根據你按下的按鈕而改變。\n\n每日任務的顏色會根據完成頻率而變化,完成的每日任務顏色會變藍,未完成的每日任務顏色會變紅。\n\n待辦事項的顏色會隨著未完成時間的延長而逐漸變紅。\n\n任務的紅色愈深,完成它獲得的金幣和經驗值就愈多,所以即使是最艱鉅的任務,也一定要挑戰!",
|
||||
"webFaqAnswer26": "正面習慣 (您想要鼓勵的行為;應帶有「+」按鈕)\n\n * 吃維他命\n * 使用牙線\n * 學習一小時\n\n負向習慣(你想要限制或避免的行為;應該有「-」按鈕)\n\n * 抽菸\n * 無止盡滑手機\n * 咬指甲\n\n雙向習慣(同時包含正面與負面選項的行為;應同時帶有「+」與「-」按鈕)\n\n * 喝水 vs. 喝汽水\n * 學習 vs. 拖延\n\n每日任務範例 (您想依循規律時程重複的任務)\n * 洗碗\n * 澆花\n * 30 分鐘的體能運動\n\n待辦事項範例(您只需要完成一次的事情)\n\n * 預約時間\n * 整理衣櫃\n * 完成報告",
|
||||
"webFaqAnswer27": "任務的顏色直觀地呈現了該任務的價值。所有任務的初始顏色都是都是代表中性的黃色,藍色代表良好,紅色則代表不良。以下是各任務類型決定其顏色的方式:\n\n習慣會根據您點擊「+」或「-」按鈕而變得更藍或更紅。如果您長時間沒有點擊,正面與負面的習慣都會隨著時間逐漸退回至黃色。而雙向習慣的顏色則只會依據您的點擊而改變。\n\n每日任務的顏色會根據完成頻率而變化,完成的每日任務顏色會變藍,未完成的每日任務顏色會變紅。\n\n待辦事項在未完成的狀態下放置越久,顏色就會逐漸變得越紅。\n\n任務的紅色愈深,完成它獲得的金幣和經驗值就愈多,所以,請務必去挑戰那些您最艱困的任務!",
|
||||
"webFaqAnswer28": "當然可以! 在「設定」中有個「暫停傷害」按鈕,可以防止你因為沒完成每日任務而損失生命值。如果你正在度假、需要休息,或因為其他原因需要暫停一下,這個功能可以幫得上忙。如果你正在打某個副本,你自己的待結算進度會暫停,但你仍然會因隊友沒完成每日任務而受到傷害。\n\n如果要暫停特定的每日任務,你可以編輯排程,改為「每 0 天到期一次」,直到你準備好重新開始為止。",
|
||||
"webFaqAnswer29": "你可以從「獎勵」欄花費 25 金幣購買「治療藥水」,即可恢復 15 點生命值。另外,每次升級後都會重新滿血!",
|
||||
"webFaqAnswer30": "如果 HP(生命值)歸零,你就會降級一等、失去所有金幣,以及一件可以重新購買的裝備。",
|
||||
@@ -115,5 +115,22 @@
|
||||
"webFaqAnswer56": "在移動端取消待處理的邀請:\n 1. 在查看你的隊伍時,向下滾動到成員列表底部\n 2. 找到您希望取消邀請的玩家,點擊“取消邀請”按鈕。\n\n在網頁端取消待處理的邀請:\n 1. 導航到隊伍的成員列表並切換到“邀請”界面\n 2. 將鼠標懸停在你想要取消邀請的玩家上\n 3. 點擊三個點並選擇“取消邀請”",
|
||||
"webFaqAnswer57": "當加入了一個隊伍,您將不能接受更多的邀請。如果您想要拒絕某個特定玩家的邀請和之後的通信,請查看該玩家的個人資料並點擊屏蔽按鈕。在移動端,需要點擊個人資料右上角的三個點,然後選擇“屏蔽”。\n\n如果您遇到並確信某位玩家的用戶名、個人資料或私信違反了社區規定,請發送信息給我們或發送電郵到admin@habitica.com。",
|
||||
"webFaqAnswer58": "目前無法從尋找隊伍的成員列表中進行篩選。但是,我們計劃將來引入諸如職業、等級和語言等篩選條件。",
|
||||
"webFaqAnswer59": "Habitica團隊計劃提供了一種共享體驗,允許隊員輕鬆地在共享任務板上添加、分配和完成任務。憑借成員角色、狀態視圖和任務分配等功能,團隊計劃非常適合擁有共同目標的家庭或同事團隊。這也是一種在與怪物戰鬥和改善生活旅程中互相激勵的好方式。"
|
||||
"webFaqAnswer59": "Habitica團隊計劃提供了一種共享體驗,允許隊員輕鬆地在共享任務板上添加、分配和完成任務。憑借成員角色、狀態視圖和任務分配等功能,團隊計劃非常適合擁有共同目標的家庭或同事團隊。這也是一種在與怪物戰鬥和改善生活旅程中互相激勵的好方式。",
|
||||
"webFaqAnswer60": "這裡有一些小提示來幫助您開啟Habitica團隊計劃的使用: \n\n * 可以提拔成員為管理員,讓他們能夠創建和編輯任務 \n * 如果是任何人都能完成且只需完成1次的任務,請讓該任務保持未分配狀態 \n * 可以將任務分配給某人可以確保其他人無法完成該任務 \n * 如果某個任務需要多人完成,可以將該任務分給多人 \n * 為防止遺漏完成多人共享任務,你的個人任務版面也可以切換顯示共享任務 \n * 即使是分配給多人的任務,你也能因為完成任務而獲得獎勵 \n * 完成任務的獎勵不會在隊員之間拆分 \n * 可以在團隊任務板上使用任務顏色來判斷任務的平均完成率 \n * 記得定期檢查共享任務板上的任務,確保共享任務仍然有效 \n * 錯過完成完成每日任务不会对你或你的团队造成伤害,但该任务的颜色会逐渐变差",
|
||||
"webFaqAnswer61": "只有團隊隊長和管理員可以創建共享任務。如果你想要某位成員也能創建任務,需要將其提升為管理員。\n\n在網頁端將團隊計劃的成員提升為管理員需要:\n 1. 導航到團隊計劃頁面並切換到“團隊信息”菜單\n 2. 查看成員列表並點擊要提升成員旁的原型按鈕\n 3. 選擇“指定為管理員”",
|
||||
"faqQuestion62": "該怎麼指派任務?",
|
||||
"webFaqAnswer62": "群組方案讓你能夠將共享任務分配給群組計劃中的其他成員。當共享任務被分配給某個成員時,其他成員將無法完成該任務。\n\n你也可以將任務分配給多個成員。例如,如果每個人都必須刷牙,建立一個任務並將它分配給每位成員。每位成員都可以完成任務並獲得各自的獎勵。當所有人都完成後,主要任務將顯示為已完成。",
|
||||
"faqQuestion63": "未分配的任務是如何運作的?",
|
||||
"webFaqAnswer63": "未分配的任務可以由任何成員完成。例如:倒垃圾。無論誰去倒垃圾,都可以完成這個未分配的任務,並且它會顯示為所有人都已完成。",
|
||||
"faqQuestion64": "同步的每日重置是如何運作的?",
|
||||
"webFaqAnswer64": "共享任務會在同一時間為所有人重置,以保持共享任務板的同步。這個時間會顯示在共享任務板上,並由群組計劃領導者的「一天開始時間」決定。\n由於共享任務會自動重置,所以當你在隔天早上登入時,將沒有機會完成前一天未完成的共享每日任務。\n\n未完成的共享每日任務不會造成傷害,但它們的顏色會逐漸退化,以幫助視覺化進度。",
|
||||
"faqQuestion65": "群組方案在行動應用程式上有支援嗎?",
|
||||
"webFaqAnswer65": "雖然行動應用程式尚未完全支援所有群組方案的功能,但你仍然可以透過 iOS 和 Android 應用程式完成共享任務!\n\n在 Android 上,檢視任務時可以點擊螢幕頂端的顯示名稱,切換到共享任務板。從那裡你可以查看成員、進入聊天,以及建立、完成或分配任務。\n\n你也可以開啟一個偏好設定,把共享任務複製到你的個人任務板,這樣就能在同一個地方完成所有任務。\n\n在行動應用程式上操作方式:\n打開設定,開啟「複製共享任務」\n\n在 Habitica 網站上操作方式:\n前往你的群組方案,並在共享任務板上開啟「複製任務」切換鍵",
|
||||
"faqQuestion66": "群組方案的共享任務與挑戰任務有什麼不同?",
|
||||
"webFaqAnswer66": "群組方案的共享任務板比挑戰更具動態性,因為它們可以不斷更新與互動。挑戰則適合在有一組固定任務要分送給許多人時使用。\n\n群組方案同時也是付費功能,而挑戰對所有人都是免費的。\n\n在挑戰中,你無法指定特定成員去完成某些任務,而且挑戰也沒有共享的每日重置機制。總體來說,挑戰提供的控制與直接互動會比較少。",
|
||||
"sunsetFaqTitle": "Habitica 酒館與公會服務終止 FAQ",
|
||||
"sunsetFaqPara1": "由於多種因素,包括我們玩家群體與 Habitica 的互動方式改變,以及新的內容規範,我們做出了艱難的決定,將於 <strong> 2023 年 8 月 8 日</strong> 終止酒館與公會服務。",
|
||||
"sunsetFaqPara4": "為了紀念我們共同度過的時光,在邁入這個新時代之際,我們將贈送所有人一隻老兵寵物。至於我們出色的貢獻者們,我們也會送上一套特別的裝備組,以紀念他們在 Habitica 社群中的所有努力。",
|
||||
"sunsetFaqPara5": "如果你想了解更多即將變動的內容,可以閱讀以下的詳細資訊。",
|
||||
"sunsetFaqPara3": "我們做出這項決定,是為了能更好地將資源集中在 Habitica 玩家最依賴的部分,同時不會影響任何人的使用權限。"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import pick from 'lodash/pick';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import nconf from 'nconf';
|
||||
import { body , validationResult } from 'express-validator';
|
||||
import {
|
||||
authWithHeaders,
|
||||
} from '../../middlewares/auth';
|
||||
@@ -87,8 +88,8 @@ api.loginLocal = {
|
||||
errorMessage: res.t('missingPassword'),
|
||||
},
|
||||
});
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
req.sanitizeBody('username').trim();
|
||||
req.sanitizeBody('password').trim();
|
||||
@@ -216,8 +217,8 @@ api.updateUsername = {
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const newUsername = req.body.username;
|
||||
|
||||
@@ -307,7 +308,7 @@ api.updatePassword = {
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
const validationErrors = validationResult(req).array();
|
||||
|
||||
if (validationErrors) {
|
||||
throw validationErrors;
|
||||
@@ -353,8 +354,8 @@ api.resetPassword = {
|
||||
notEmpty: { errorMessage: res.t('missingEmail') },
|
||||
},
|
||||
});
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const email = req.body.email.toLowerCase();
|
||||
let user = await User.findOne(
|
||||
@@ -419,12 +420,12 @@ api.updateEmail = {
|
||||
|
||||
if (!user.auth.local.email) throw new BadRequest(res.t('userHasNoLocalRegistration'));
|
||||
|
||||
req.checkBody('newEmail', res.t('newEmailRequired')).notEmpty().isEmail();
|
||||
await body('newEmail', res.t('newEmailRequired')).notEmpty().isEmail().run(req)
|
||||
if (user.auth.local.hashed_password) {
|
||||
req.checkBody('password', res.t('missingPassword')).notEmpty();
|
||||
await body('password', res.t('missingPassword')).notEmpty().run(req)
|
||||
}
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const emailAlreadyInUse = await User.findOne({
|
||||
'auth.local.email': req.body.newEmail.toLowerCase(),
|
||||
@@ -485,8 +486,8 @@ api.resetPasswordSetNewOne = {
|
||||
},
|
||||
});
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { newPassword, confirmPassword } = req.body;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import merge from 'lodash/merge';
|
||||
import pick from 'lodash/pick';
|
||||
import reduce from 'lodash/reduce';
|
||||
import times from 'lodash/times';
|
||||
import { body, param, query , validationResult } from 'express-validator';
|
||||
import { authWithHeaders, authWithSession } from '../../middlewares/auth';
|
||||
import { model as Challenge } from '../../models/challenge';
|
||||
import bannedWords from '../../libs/bannedWords';
|
||||
@@ -232,11 +233,11 @@ api.createChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkBody('group', apiError('groupIdRequired')).notEmpty();
|
||||
req.checkBody('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_CHALLENGES });
|
||||
await body('group', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
await body('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_CHALLENGES }).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({
|
||||
user, groupId: req.body.group, fields: basicGroupFields, optionalMembership: true,
|
||||
@@ -328,10 +329,10 @@ api.joinChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -397,10 +398,10 @@ api.leaveChallenge = {
|
||||
const { user } = res.locals;
|
||||
const keep = req.body.keep === 'remove-all' ? 'remove-all' : 'keep-all';
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -463,10 +464,10 @@ api.getUserChallenges = {
|
||||
url: '/challenges/user',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('page').notEmpty().isInt({ min: 0 }, apiError('queryPageInteger'));
|
||||
await query('page').notEmpty().isInt({ min: 0 }, apiError('queryPageInteger')).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const CHALLENGES_PER_PAGE = 10;
|
||||
const {
|
||||
@@ -599,10 +600,10 @@ api.getGroupChallenges = {
|
||||
const { user } = res.locals;
|
||||
let { groupId } = req.params;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
if (groupId === 'party') groupId = user.party._id;
|
||||
if (groupId === 'habitrpg') groupId = TAVERN_ID;
|
||||
@@ -661,10 +662,10 @@ api.getChallenge = {
|
||||
url: '/challenges/:challengeId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { challengeId } = req.params;
|
||||
@@ -719,10 +720,10 @@ api.exportChallengeCsv = {
|
||||
url: '/challenges/:challengeId/export/csv',
|
||||
middlewares: [authWithSession],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { challengeId } = req.params;
|
||||
@@ -836,11 +837,11 @@ api.updateChallenge = {
|
||||
url: '/challenges/:challengeId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkBody('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_CHALLENGES });
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
await body('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_CHALLENGES }).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { challengeId } = req.params;
|
||||
@@ -883,10 +884,10 @@ api.deleteChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -931,11 +932,11 @@ api.selectChallengeWinner = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('winnerId', res.t('winnerIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
await param('winnerId', res.t('winnerIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -992,10 +993,10 @@ api.cloneChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challengeToClone = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challengeToClone) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -1061,10 +1062,10 @@ api.flagChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const challenge = await Challenge.findOne({ _id: req.params.challengeId }).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -1094,10 +1095,10 @@ api.clearFlagsChallenge = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
if (!user.hasPermission('moderator')) {
|
||||
throw new NotAuthorized(res.t('messageGroupChatAdminClearFlagCount'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import couponCode from 'coupon-code';
|
||||
import { param, query , validationResult } from 'express-validator';
|
||||
import csvStringify from '../../libs/csvStringify';
|
||||
import {
|
||||
authWithHeaders,
|
||||
@@ -72,11 +73,11 @@ api.generateCoupons = {
|
||||
url: '/coupons/generate/:event',
|
||||
middlewares: [authWithHeaders(), ensurePermission('coupons')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('event', apiError('eventRequired')).notEmpty();
|
||||
req.checkQuery('count', apiError('countRequired')).notEmpty().isNumeric();
|
||||
await param('event', apiError('eventRequired')).notEmpty().run(req)
|
||||
await query('count', apiError('countRequired')).notEmpty().isNumeric().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const coupons = await Coupon.generate(req.params.event, req.query.count);
|
||||
res.respond(200, coupons);
|
||||
@@ -122,10 +123,10 @@ api.validateCoupon = {
|
||||
optional: true,
|
||||
})],
|
||||
async handler (req, res) {
|
||||
req.checkParams('code', res.t('couponCodeRequired')).notEmpty();
|
||||
await param('code', res.t('couponCodeRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
let valid = false;
|
||||
const code = couponCode.validate(req.params.code);
|
||||
|
||||
@@ -9,6 +9,7 @@ import pick from 'lodash/pick';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import nconf from 'nconf';
|
||||
import moment from 'moment';
|
||||
import { body, param, query , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
model as Group,
|
||||
@@ -132,10 +133,10 @@ api.createGroup = {
|
||||
const group = new Group(Group.sanitize(req.body));
|
||||
group.leader = user._id;
|
||||
|
||||
req.checkBody('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS });
|
||||
await body('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS }).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
if (group.type === 'guild') {
|
||||
if (!user.hasPermission('fullAccess')) {
|
||||
@@ -206,10 +207,10 @@ api.createGroupPlan = {
|
||||
const { user } = res.locals;
|
||||
const group = new Group(Group.sanitize(req.body.groupToCreate));
|
||||
|
||||
req.checkBody('paymentType', res.t('paymentTypeRequired')).notEmpty();
|
||||
req.checkBody('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS });
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
await body('paymentType', res.t('paymentTypeRequired')).notEmpty().run(req)
|
||||
await body('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS }).run(req)
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
// @TODO: Change message
|
||||
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
|
||||
@@ -321,13 +322,13 @@ api.getGroups = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkQuery('type', res.t('groupTypesRequired')).notEmpty();
|
||||
await query('type', res.t('groupTypesRequired')).notEmpty().run(req)
|
||||
// pagination options, can only be used with public guilds
|
||||
req.checkQuery('paginate').optional().isIn(['true', 'false'], apiError('guildsPaginateBooleanString'));
|
||||
req.checkQuery('page').optional().isInt({ min: 0 }, apiError('queryPageInteger'));
|
||||
await query('paginate').optional().isIn(['true', 'false'], apiError('guildsPaginateBooleanString')).run(req)
|
||||
await query('page').optional().isInt({ min: 0 }, apiError('queryPageInteger')).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const types = req.query.type.split(',');
|
||||
|
||||
@@ -419,10 +420,10 @@ api.getGroup = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
const group = await Group.getGroup({ user, groupId, populateLeader: false });
|
||||
@@ -481,11 +482,11 @@ api.updateGroup = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
req.checkBody('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS });
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
await body('summary', apiError('summaryLengthExceedsMax')).isLength({ max: MAX_SUMMARY_SIZE_FOR_GUILDS }).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
const optionalMembership = Boolean(user.hasPermission('moderator'));
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, optionalMembership });
|
||||
|
||||
@@ -567,10 +568,10 @@ api.joinGroup = {
|
||||
const { user } = res.locals;
|
||||
let inviter;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
// Works even if the user is not yet a member of the group
|
||||
// Do not fetch chat and work even if the user is not yet a member of the group
|
||||
@@ -760,10 +761,10 @@ api.rejectGroupInvite = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
let isUserInvited = false;
|
||||
@@ -831,13 +832,13 @@ api.leaveGroup = {
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
// When removing the user from challenges, should we keep the tasks?
|
||||
req.checkQuery('keep', apiError('keepOrRemoveAll')).optional().isIn(['keep-all', 'remove-all']);
|
||||
req.checkBody('keepChallenges', apiError('groupRemainOrLeaveChallenges')).optional().isIn(['remain-in-challenges', 'leave-challenges']);
|
||||
await query('keep', apiError('keepOrRemoveAll')).optional().isIn(['keep-all', 'remove-all']).run(req)
|
||||
await body('keepChallenges', apiError('groupRemainOrLeaveChallenges')).optional().isIn(['remain-in-challenges', 'leave-challenges']).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
await leaveGroup({
|
||||
@@ -899,11 +900,11 @@ api.removeGroupMember = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
req.checkParams('memberId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
await param('memberId', res.t('userIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
const optionalMembership = Boolean(user.hasPermission('moderator'));
|
||||
const group = await Group.getGroup({
|
||||
user, groupId: req.params.groupId, optionalMembership, fields: '-chat',
|
||||
@@ -1105,12 +1106,12 @@ api.inviteToGroup = {
|
||||
|
||||
if (user.flags.chatRevoked) throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req);
|
||||
|
||||
if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: '-chat' });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
@@ -1182,11 +1183,11 @@ api.addGroupManager = {
|
||||
const { user } = res.locals;
|
||||
const { managerId } = req.body;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
req.checkBody('managerId', apiError('managerIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
await body('managerId', apiError('managerIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const newManager = await User.findById(managerId, 'guilds party').exec();
|
||||
const groupFields = basicGroupFields.concat(' managers');
|
||||
@@ -1232,11 +1233,11 @@ api.removeGroupManager = {
|
||||
const { user } = res.locals;
|
||||
const { managerId } = req.body;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
req.checkBody('managerId', apiError('managerIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
await body('managerId', apiError('managerIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const groupFields = basicGroupFields.concat(' managers');
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: groupFields });
|
||||
@@ -1323,7 +1324,7 @@ api.getLookingForParty = {
|
||||
const USERS_PER_PAGE = 30;
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkQuery('page').optional().isInt({ min: 0 }, apiError('queryPageInteger'));
|
||||
await query('page').optional().isInt({ min: 0 }, apiError('queryPageInteger')).run(req)
|
||||
const PAGE = req.query.page || 0;
|
||||
const PAGE_START = USERS_PER_PAGE * PAGE;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import validator from 'validator';
|
||||
import { query, param , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { model as User } from '../../models/user';
|
||||
@@ -73,10 +74,10 @@ api.getPatrons = {
|
||||
url: '/hall/patrons',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkQuery('page').optional().isInt({ min: 0 }, apiError('queryPageInteger'));
|
||||
await query('page').optional().isInt({ min: 0 }, apiError('queryPageInteger')).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const page = req.query.page ? Number(req.query.page) : 0;
|
||||
const perPage = 50;
|
||||
@@ -177,10 +178,10 @@ api.getHero = {
|
||||
url: '/hall/heroes/:heroId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
|
||||
await param('heroId', res.t('heroIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { heroId } = req.params;
|
||||
|
||||
@@ -263,10 +264,10 @@ api.updateHero = {
|
||||
const { heroId } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty().isUUID();
|
||||
await param('heroId', res.t('heroIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const hero = await User.findById(heroId).exec();
|
||||
if (!hero) throw new NotFound(res.t('userWithIDNotFound', { userId: heroId }));
|
||||
@@ -320,16 +321,16 @@ api.updateHero = {
|
||||
if (plan.extraMonths || plan.extraMonths === 0) {
|
||||
hero.purchased.plan.extraMonths = plan.extraMonths;
|
||||
}
|
||||
if (plan.customerId) {
|
||||
if (plan.customerId || plan.customerId === '') {
|
||||
hero.purchased.plan.customerId = plan.customerId;
|
||||
}
|
||||
if (plan.paymentMethod) {
|
||||
if (plan.paymentMethod || plan.customerId === '') {
|
||||
hero.purchased.plan.paymentMethod = plan.paymentMethod;
|
||||
}
|
||||
if (plan.planId) {
|
||||
if (plan.planId || plan.customerId === '') {
|
||||
hero.purchased.plan.planId = plan.planId;
|
||||
}
|
||||
if (plan.owner) {
|
||||
if (plan.owner || plan.customerId === '') {
|
||||
hero.purchased.plan.owner = plan.owner;
|
||||
}
|
||||
if (plan.hourglassPromoReceived) {
|
||||
@@ -341,8 +342,7 @@ api.updateHero = {
|
||||
const group = await Group.getGroup({ user: hero, groupId: groupID });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.hasNotCancelled()) {
|
||||
hero.purchased.plan.customerId = null;
|
||||
hero.purchased.plan.paymentMethod = null;
|
||||
hero.purchased.plan.paymentMethod = 'groupPlan';
|
||||
await addSubToGroupUser(hero, group);
|
||||
await group.updateGroupPlan();
|
||||
} else {
|
||||
@@ -352,34 +352,34 @@ api.updateHero = {
|
||||
}
|
||||
|
||||
if (updateData.stats) {
|
||||
if (updateData.stats.hp) {
|
||||
if (updateData.stats.hp || updateData.stats.hp === 0) {
|
||||
hero.stats.hp = updateData.stats.hp;
|
||||
}
|
||||
if (updateData.stats.mp) {
|
||||
if (updateData.stats.mp || updateData.stats.mp === 0) {
|
||||
hero.stats.mp = updateData.stats.mp;
|
||||
}
|
||||
if (updateData.stats.exp) {
|
||||
if (updateData.stats.exp || updateData.stats.exp === 0) {
|
||||
hero.stats.exp = updateData.stats.exp;
|
||||
}
|
||||
if (updateData.stats.gp) {
|
||||
if (updateData.stats.gp || updateData.stats.gp === 0) {
|
||||
hero.stats.gp = updateData.stats.gp;
|
||||
}
|
||||
if (updateData.stats.lvl) {
|
||||
if (updateData.stats.lvl || updateData.stats.lvl === 0) {
|
||||
hero.stats.lvl = updateData.stats.lvl;
|
||||
}
|
||||
if (updateData.stats.points) {
|
||||
if (updateData.stats.points || updateData.stats.points === 0) {
|
||||
hero.stats.points = updateData.stats.points;
|
||||
}
|
||||
if (updateData.stats.str) {
|
||||
if (updateData.stats.str || updateData.stats.str === 0) {
|
||||
hero.stats.str = updateData.stats.str;
|
||||
}
|
||||
if (updateData.stats.int) {
|
||||
if (updateData.stats.int || updateData.stats.int === 0) {
|
||||
hero.stats.int = updateData.stats.int;
|
||||
}
|
||||
if (updateData.stats.per) {
|
||||
if (updateData.stats.per || updateData.stats.per === 0) {
|
||||
hero.stats.per = updateData.stats.per;
|
||||
}
|
||||
if (updateData.stats.con) {
|
||||
if (updateData.stats.con || updateData.stats.con === 0) {
|
||||
hero.stats.con = updateData.stats.con;
|
||||
}
|
||||
if (updateData.stats.buffs) {
|
||||
@@ -556,10 +556,10 @@ api.getHeroParty = { // @TODO XXX add tests
|
||||
url: '/hall/heroes/party/:groupId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
|
||||
@@ -598,10 +598,10 @@ api.getHeroGroupPlans = {
|
||||
url: '/hall/heroes/:heroId/group-plans',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('heroId', res.t('heroIdRequired')).notEmpty();
|
||||
await param('heroId', res.t('heroIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { heroId } = req.params;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
import pick from 'lodash/pick';
|
||||
import { body, param, query as checkQuery , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
model as User,
|
||||
@@ -103,10 +104,10 @@ api.getMember = {
|
||||
url: '/members/:memberId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
await param('memberId', res.t('memberIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { memberId } = req.params;
|
||||
|
||||
@@ -132,10 +133,10 @@ api.getMemberByUsername = {
|
||||
url: '/members/username/:username',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('username', res.t('invalidReqParams')).notEmpty();
|
||||
await param('username', res.t('invalidReqParams')).notEmpty().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
let username = req.params.username.toLowerCase();
|
||||
if (username[0] === '@') username = username.slice(1, username.length);
|
||||
@@ -261,10 +262,10 @@ api.getMemberAchievements = {
|
||||
url: '/members/:memberId/achievements',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
await param('memberId', res.t('memberIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { memberId } = req.params;
|
||||
|
||||
@@ -292,13 +293,13 @@ function _getMembersForItem (type) {
|
||||
}
|
||||
|
||||
return async function handleGetMembersForItem (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty();
|
||||
req.checkQuery('lastId').optional().notEmpty().isUUID();
|
||||
await param('groupId', res.t('groupIdRequired')).notEmpty().run(req);
|
||||
await checkQuery('lastId').optional().notEmpty().isUUID().run(req)
|
||||
// Allow an arbitrary number of results (up to 60)
|
||||
req.checkQuery('limit', res.t('groupIdRequired')).optional().notEmpty().isInt({ min: 1, max: 60 });
|
||||
await checkQuery('limit', res.t('groupIdRequired')).optional().notEmpty().isInt({ min: 1, max: 60 }).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
const { lastId } = req.query;
|
||||
@@ -566,11 +567,11 @@ api.getChallengeMemberProgress = {
|
||||
url: '/challenges/:challengeId/members/:memberId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req);
|
||||
await param('memberId', res.t('memberIdRequired')).notEmpty().isUUID().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { challengeId } = req.params;
|
||||
@@ -627,11 +628,11 @@ api.getObjectionsToInteraction = {
|
||||
url: '/members/:toUserId/objections/:interaction',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
|
||||
req.checkParams('interaction', res.t('interactionRequired')).notEmpty().isIn(KNOWN_INTERACTIONS);
|
||||
await param('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID().run(req);
|
||||
await param('interaction', res.t('interactionRequired')).notEmpty().isIn(KNOWN_INTERACTIONS).run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const sender = res.locals.user;
|
||||
const receiver = await User.findById(req.params.toUserId).exec();
|
||||
@@ -662,11 +663,11 @@ api.transferGems = {
|
||||
url: '/members/transfer-gems',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkBody('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
|
||||
req.checkBody('gemAmount', res.t('gemAmountRequired')).notEmpty().isInt();
|
||||
await body('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID().run(req)
|
||||
await body('gemAmount', res.t('gemAmountRequired')).notEmpty().isInt().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const sender = res.locals.user;
|
||||
const receiver = await User.findById(req.body.toUserId).exec();
|
||||
@@ -815,9 +816,9 @@ api.clearUserFlags = {
|
||||
const { user } = res.locals;
|
||||
const { memberId } = req.params;
|
||||
|
||||
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
await param('memberId', res.t('memberIdRequired')).notEmpty().isUUID().run(req);
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
if (!user.hasPermission('moderator')) {
|
||||
throw new BadRequest('Only a moderator may clear reports from a profile.');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { param , validationResult } from 'express-validator';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -36,10 +37,10 @@ api.getModelPaths = {
|
||||
method: 'GET',
|
||||
url: '/models/:model/paths',
|
||||
async handler (req, res) {
|
||||
req.checkParams('model', res.t('modelNotFound')).notEmpty().isIn(allModels);
|
||||
await param('model', res.t('modelNotFound')).notEmpty().isIn(allModels).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
let { model } = req.params;
|
||||
// tasks models are lowercase, the others have the first letter uppercase (User, Group)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { body, param , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
NotFound,
|
||||
@@ -25,11 +26,11 @@ api.addPushDevice = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
|
||||
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
|
||||
await body('regId', res.t('regIdRequired')).notEmpty().run(req)
|
||||
await body('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { pushDevices } = user;
|
||||
|
||||
@@ -75,10 +76,10 @@ api.removePushDevice = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
|
||||
await param('regId', res.t('regIdRequired')).notEmpty().run(req);
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { regId } = req.params;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import each from 'lodash/each';
|
||||
import every from 'lodash/every';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
import pick from 'lodash/pick';
|
||||
import { param , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
|
||||
import {
|
||||
@@ -68,10 +69,10 @@ api.inviteToQuest = {
|
||||
const { questKey } = req.params;
|
||||
const quest = questScrolls[questKey];
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: basicGroupFields.concat(' quest chat') });
|
||||
|
||||
@@ -202,10 +203,10 @@ api.acceptQuest = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: basicGroupFields.concat(' quest chat') });
|
||||
|
||||
@@ -267,10 +268,10 @@ api.rejectQuest = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: basicGroupFields.concat(' quest chat') });
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
@@ -335,10 +336,10 @@ api.forceStart = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId: req.params.groupId, fields: basicGroupFields.concat(' quest chat') });
|
||||
|
||||
@@ -400,10 +401,10 @@ api.cancelQuest = {
|
||||
const { user } = res.locals;
|
||||
const { groupId } = req.params;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId, fields: basicGroupFields.concat(' quest') });
|
||||
|
||||
@@ -475,10 +476,10 @@ api.abortQuest = {
|
||||
const { user } = res.locals;
|
||||
const { groupId } = req.params;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId, fields: basicGroupFields.concat(' quest chat') });
|
||||
|
||||
@@ -549,10 +550,10 @@ api.leaveQuest = {
|
||||
const { user } = res.locals;
|
||||
const { groupId } = req.params;
|
||||
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const group = await Group.getGroup({ user, groupId, fields: basicGroupFields.concat(' quest') });
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import assign from 'lodash/assign';
|
||||
import find from 'lodash/find';
|
||||
import merge from 'lodash/merge';
|
||||
import pick from 'lodash/pick';
|
||||
import { param, query , validationResult } from 'express-validator'
|
||||
import moment from 'moment';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import {
|
||||
@@ -310,9 +311,9 @@ api.createChallengeTasks = {
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
@@ -394,10 +395,10 @@ api.getUserTasks = {
|
||||
async handler (req, res) {
|
||||
const types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
req.checkQuery('type', res.t('invalidTasksTypeExtra')).optional().isIn(types);
|
||||
await query('type', res.t('invalidTasksTypeExtra')).optional().isIn(types).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { dueDate } = req.query;
|
||||
@@ -447,12 +448,12 @@ api.getChallengeTasks = {
|
||||
url: '/tasks/challenge/:challengeId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req)
|
||||
const types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||
await query('type', res.t('invalidTasksType')).optional().isIn(types).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { challengeId } = req.params;
|
||||
@@ -602,10 +603,10 @@ api.updateTask = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -805,11 +806,11 @@ api.moveTask = {
|
||||
url: '/tasks/:taskId/move/to/:position',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('position', res.t('positionRequired')).notEmpty().isNumeric().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { taskId } = req.params;
|
||||
@@ -908,10 +909,10 @@ api.addChecklistItem = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -957,11 +958,11 @@ api.scoreCheckListItem = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('itemId', res.t('itemIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -1018,11 +1019,11 @@ api.updateChecklistItem = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('itemId', res.t('itemIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -1079,11 +1080,11 @@ api.removeChecklistItem = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('itemId', res.t('itemIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('itemId', res.t('itemIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id);
|
||||
@@ -1141,12 +1142,12 @@ api.addTagToTask = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
const userTags = user.tags.map(tag => tag.id);
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID().isIn(userTags);
|
||||
await param('tagId', res.t('tagIdRequired')).notEmpty().isUUID().isIn(userTags).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id, { userId: user._id });
|
||||
@@ -1198,11 +1199,11 @@ api.removeTagFromTask = {
|
||||
async handler (req, res) {
|
||||
const { user } = res.locals;
|
||||
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('tagId', res.t('tagIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('tagId', res.t('tagIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { taskId } = req.params;
|
||||
const task = await Tasks.Task.findByIdOrAlias(taskId, user._id, { userId: user._id });
|
||||
@@ -1243,11 +1244,11 @@ api.unlinkAllTasks = {
|
||||
url: '/tasks/unlink-all/:challengeId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']);
|
||||
await param('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID().run(req)
|
||||
await query('keep', apiError('keepOrRemoveAll')).notEmpty().isIn(['keep-all', 'remove-all']).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { keep } = req.query;
|
||||
@@ -1309,11 +1310,11 @@ api.unlinkOneTask = {
|
||||
url: '/tasks/unlink-one/:taskId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']);
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().isUUID().run(req)
|
||||
await query('keep', apiError('keepOrRemove')).notEmpty().isIn(['keep', 'remove']).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { keep } = req.query;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pick from 'lodash/pick';
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { param , query , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import * as Tasks from '../../../models/task';
|
||||
import { model as Group } from '../../../models/group';
|
||||
@@ -45,9 +46,9 @@ api.createGroupTasks = {
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
@@ -92,11 +93,11 @@ api.getGroupTasks = {
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', apiError('groupIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||
await param('groupId', apiError('groupIdRequired')).notEmpty().isUUID().run(req)
|
||||
await query('type', res.t('invalidTasksType')).optional().isIn(types).run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -132,10 +133,10 @@ api.groupMoveTask = {
|
||||
url: '/group-tasks/:taskId/move/to/:position',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().run(req)
|
||||
await param('position', res.t('positionRequired')).notEmpty().isNumeric().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
@@ -203,9 +204,9 @@ api.assignTask = {
|
||||
url: '/tasks/:taskId/assign',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
@@ -280,10 +281,10 @@ api.unassignTask = {
|
||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().isUUID().run(req)
|
||||
await param('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
@@ -337,10 +338,10 @@ api.taskNeedsWork = {
|
||||
url: '/tasks/:taskId/needs-work/:userId',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', apiError('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
await param('taskId', apiError('taskIdRequired')).notEmpty().isUUID().run(req)
|
||||
await param('userId', res.t('userIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const reqValidationErrors = req.validationErrors();
|
||||
const reqValidationErrors = validationResult(req).array();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
@@ -4,6 +4,7 @@ import isFunction from 'lodash/isFunction';
|
||||
import pick from 'lodash/pick';
|
||||
import nconf from 'nconf';
|
||||
import get from 'lodash/get';
|
||||
import { param , validationResult } from 'express-validator';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import common from '../../../common';
|
||||
import {
|
||||
@@ -1735,11 +1736,11 @@ api.movePinnedItem = {
|
||||
url: '/user/move-pinned-item/:path/move/to/:position',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
|
||||
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
||||
await param('path', res.t('taskIdRequired')).notEmpty().run(req)
|
||||
await param('position', res.t('positionRequired')).notEmpty().isNumeric().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { user } = res.locals;
|
||||
const { path } = req.params;
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import validator from 'validator';
|
||||
import merge from 'lodash/merge';
|
||||
import { param , validationResult } from 'express-validator';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { model as User } from '../../models/user';
|
||||
import { model as UserHistory } from '../../models/userHistory';
|
||||
import { model as Group } from '../../models/group';
|
||||
import { model as Blocker } from '../../models/blocker';
|
||||
import {
|
||||
NotFound,
|
||||
} from '../../libs/errors';
|
||||
import apple from '../../libs/payments/apple';
|
||||
import google from '../../libs/payments/google';
|
||||
import paypal from '../../libs/payments/paypal';
|
||||
import {
|
||||
getSubscriptionPaymentDetails as getStripeSubscriptionPaymentDetails,
|
||||
} from '../../libs/payments/stripe/subscriptions';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -33,15 +42,13 @@ api.searchHero = {
|
||||
url: '/admin/search/:userIdentifier',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('userIdentifier', res.t('userIdentifierRequired')).notEmpty();
|
||||
await param('userIdentifier', res.t('userIdentifierRequired')).notEmpty().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { userIdentifier } = req.params;
|
||||
|
||||
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
|
||||
let query;
|
||||
let users = [];
|
||||
if (validator.isUUID(userIdentifier)) {
|
||||
@@ -54,7 +61,7 @@ api.searchHero = {
|
||||
'auth.facebook.emails.value',
|
||||
];
|
||||
for (const field of emailFields) {
|
||||
const emailQuery = { [field]: userIdentifier };
|
||||
const emailQuery = { [field]: userIdentifier.toLowerCase() };
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const found = await User.findOne(emailQuery)
|
||||
.select('contributor backer profile auth')
|
||||
@@ -65,6 +72,7 @@ api.searchHero = {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const re = new RegExp(String.raw`^${userIdentifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
query = { 'auth.local.lowerCaseUsername': { $regex: re, $options: 'i' } };
|
||||
}
|
||||
|
||||
@@ -76,7 +84,8 @@ api.searchHero = {
|
||||
.lean()
|
||||
.exec();
|
||||
}
|
||||
res.respond(200, users);
|
||||
|
||||
res.respond(200, uniqBy(users, '_id'));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,10 +110,10 @@ api.getUserHistory = {
|
||||
url: '/admin/user/:userId/history',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('userId', res.t('heroIdRequired')).notEmpty().isUUID();
|
||||
await param('userId', res.t('heroIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const { userId } = req.params;
|
||||
|
||||
@@ -153,10 +162,10 @@ api.updateBlocker = {
|
||||
url: '/admin/blockers/:blockerId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('accessControl')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('blockerId', res.t('blockerIdRequired')).notEmpty().isUUID();
|
||||
await param('blockerId', res.t('blockerIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const blocker = await Blocker.findById(req.params.blockerId).exec();
|
||||
if (!blocker) throw new NotFound(res.t('blockerNotFound'));
|
||||
@@ -173,10 +182,10 @@ api.deleteBlocker = {
|
||||
url: '/admin/blockers/:blockerId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('accessControl')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('blockerId', res.t('blockerIdRequired')).notEmpty().isUUID();
|
||||
await param('blockerId', res.t('blockerIdRequired')).notEmpty().isUUID().run(req)
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
const validationErrors = validationResult(req).array();
|
||||
if (validationErrors && validationErrors.length > 0) throw validationErrors;
|
||||
|
||||
const blocker = await Blocker.findById(req.params.blockerId).exec();
|
||||
if (!blocker) throw new NotFound(res.t('blockerNotFound'));
|
||||
@@ -188,4 +197,68 @@ api.deleteBlocker = {
|
||||
},
|
||||
};
|
||||
|
||||
api.validateSubscriptionPaymentDetails = {
|
||||
method: 'GET',
|
||||
url: '/admin/user/:userId/subscription-payment-details',
|
||||
middlewares: [authWithHeaders(), ensurePermission('userSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('userId', res.t('heroIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { userId } = req.params;
|
||||
|
||||
const user = await User.findById(userId)
|
||||
.select('purchased')
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!user) throw new NotFound(res.t('userWithIDNotFound', { userId }));
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.paymentMethod || !user.purchased.plan.paymentMethod === '') {
|
||||
throw new NotFound(res.t('subscriptionNotFoundForUser', { userId }));
|
||||
}
|
||||
|
||||
let paymentDetails;
|
||||
if (user.purchased.plan.paymentMethod === 'Apple') {
|
||||
paymentDetails = await apple.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||
} else if (user.purchased.plan.paymentMethod === 'Google') {
|
||||
paymentDetails = await google.getSubscriptionPaymentDetails(userId, user.purchased.plan);
|
||||
} else if (user.purchased.plan.paymentMethod === 'Paypal') {
|
||||
paymentDetails = await paypal.getSubscriptionPaymentDetails({ user });
|
||||
} else if (user.purchased.plan.paymentMethod === 'Stripe') {
|
||||
paymentDetails = await getStripeSubscriptionPaymentDetails(user);
|
||||
} else if (user.purchased.plan.paymentMethod === 'Amazon Payments') {
|
||||
throw new NotFound(res.t('amazonSubscriptionNotValidated'));
|
||||
} else if (user.purchased.plan.paymentMethod === 'Gift') {
|
||||
throw new NotFound(res.t('giftSubscriptionNotValidated'));
|
||||
} else {
|
||||
throw new NotFound(res.t('unknownSubscriptionPaymentMethod', { method: user.purchased.paymentMethod }));
|
||||
}
|
||||
res.respond(200, paymentDetails);
|
||||
},
|
||||
};
|
||||
|
||||
api.getGroup = {
|
||||
method: 'GET',
|
||||
url: '/admin/groups/:groupId',
|
||||
middlewares: [authWithHeaders(), ensurePermission('groupSupport')],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
const { groupId } = req.params;
|
||||
|
||||
const group = await Group.findById(groupId)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
res.respond(200, group);
|
||||
},
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user