mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-27 19:22:55 +01:00
Compare commits
104 Commits
fiz/item-c
...
phillip/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebb58e4470 | ||
|
|
c3ef26b2f3 | ||
|
|
108bd59296 | ||
|
|
5228ed40d1 | ||
|
|
becb6e49f0 | ||
|
|
aa8f0f0c4e | ||
|
|
d1891f4c43 | ||
|
|
95494c685b | ||
|
|
10978d46ab | ||
|
|
447eb6a0c4 | ||
|
|
3dec49b72c | ||
|
|
472d03f276 | ||
|
|
fd9a27c3ab | ||
|
|
a5c1423837 | ||
|
|
2381be8c46 | ||
|
|
5c7545f32a | ||
|
|
ffed5a9a97 | ||
|
|
cd58ce2233 | ||
|
|
a2b5e3621e | ||
|
|
e9829b8b60 | ||
|
|
7ecb83dc7e | ||
|
|
e8ffe2286c | ||
|
|
a06dfc9ed8 | ||
|
|
58b0e323a3 | ||
|
|
9ca60d7551 | ||
|
|
6f63583a12 | ||
|
|
fe63436a57 | ||
|
|
5b93b9b37a | ||
|
|
1d55027791 | ||
|
|
83f0984da1 | ||
|
|
53d4f75cab | ||
|
|
da45eb2adf | ||
|
|
3bf4af8d8b | ||
|
|
f030691fac | ||
|
|
d952239d35 | ||
|
|
2c7f6fd9e3 | ||
|
|
ddba450630 | ||
|
|
187238d39a | ||
|
|
1f94e51693 | ||
|
|
86e7d7a72b | ||
|
|
140b852e03 | ||
|
|
8f949ce1cc | ||
|
|
5e21285370 | ||
|
|
7a65bc2d8d | ||
|
|
a32fadbcbd | ||
|
|
305192ed1f | ||
|
|
7644e202c9 | ||
|
|
d11c8442ef | ||
|
|
d8b5391425 | ||
|
|
dd287cd719 | ||
|
|
e809d1f6e4 | ||
|
|
da90fa6aaf | ||
|
|
77392db25a | ||
|
|
1bc1bf0621 | ||
|
|
635a258d62 | ||
|
|
384fb505c1 | ||
|
|
3e0bc36373 | ||
|
|
0a431afaaf | ||
|
|
8c911bcd41 | ||
|
|
dcb7ac5955 | ||
|
|
fb730942a0 | ||
|
|
9c92bf73f5 | ||
|
|
58f195fdb7 | ||
|
|
4b86c9c8a7 | ||
|
|
4cc689ec63 | ||
|
|
8690484f5e | ||
|
|
1f3e5b7a76 | ||
|
|
61c790f291 | ||
|
|
b3440fa3a8 | ||
|
|
a3f1835d1d | ||
|
|
9226f6f70e | ||
|
|
1130f9957f | ||
|
|
ad1fd03aad | ||
|
|
6c93033ad2 | ||
|
|
dd97b11b60 | ||
|
|
59ba07d4f3 | ||
|
|
d2bfd1e3a9 | ||
|
|
a8264bf526 | ||
|
|
f202f2b3d3 | ||
|
|
4ea9f8282e | ||
|
|
205d84a111 | ||
|
|
5810853cc2 | ||
|
|
4547204bd8 | ||
|
|
f17a0c91a3 | ||
|
|
16e1523b08 | ||
|
|
0f06ec1ab8 | ||
|
|
641266122a | ||
|
|
5ba939ee9c | ||
|
|
c979e568f1 | ||
|
|
93f0c240f9 | ||
|
|
ad04b077a4 | ||
|
|
7ffc454320 | ||
|
|
dae0fbff16 | ||
|
|
5648092112 | ||
|
|
275b15b773 | ||
|
|
1025635e34 | ||
|
|
836cbdb81e | ||
|
|
be922de7ba | ||
|
|
3a2f5e724d | ||
|
|
8a105c6a14 | ||
|
|
7f1c64a50e | ||
|
|
125f472f34 | ||
|
|
bafd273475 | ||
|
|
365cb1c2eb |
@@ -8,18 +8,26 @@
|
||||
"AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
|
||||
"AMPLITUDE_KEY": "AMPLITUDE_KEY",
|
||||
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"BASE_URL": "http://localhost:3000",
|
||||
"BLOCKED_IPS": "",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"CRON_SAFE_MODE": "false",
|
||||
"CRON_SEMI_SAFE_MODE": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"DISABLE_REQUEST_LOGGING": "true",
|
||||
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
||||
"EMAIL_SERVER_AUTH_PASSWORD": "password",
|
||||
"EMAIL_SERVER_AUTH_USER": "user",
|
||||
"EMAIL_SERVER_URL": "http://example.com",
|
||||
"EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
|
||||
"EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
|
||||
"ENABLE_CONSOLE_LOGS_IN_PROD": "false",
|
||||
"ENABLE_CONSOLE_LOGS_IN_TEST": "false",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"FACEBOOK_KEY": "123456789012345",
|
||||
"FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
|
||||
@@ -29,15 +37,16 @@
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"IGNORE_REDIRECT": "true",
|
||||
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"LOGGLY_CLIENT_TOKEN": "token",
|
||||
"LOGGLY_SUBDOMAIN": "example-subdomain",
|
||||
"LOGGLY_TOKEN": "example-token",
|
||||
"LOG_REQUESTS_EXCESSIVE_MODE": "false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"MONGODB_POOL_SIZE": "10",
|
||||
"MONGODB_SOCKET_TIMEOUT": "20000",
|
||||
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
|
||||
"NODE_ENV": "development",
|
||||
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
|
||||
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
|
||||
@@ -55,44 +64,34 @@
|
||||
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"PORT": 3000,
|
||||
"PUSH_CONFIGS_APN_ENABLED": "false",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
|
||||
"PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
|
||||
"PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PASSWORD": "12345678",
|
||||
"REDIS_PORT": "1234",
|
||||
"S3_ACCESS_KEY_ID": "accessKeyId",
|
||||
"S3_BUCKET": "bucket",
|
||||
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SESSION_SECRET_IV": "12345678912345678912345678912345",
|
||||
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
|
||||
"SESSION_SECRET": "YOUR SECRET HERE",
|
||||
"SITE_HTTP_AUTH_ENABLED": "false",
|
||||
"SITE_HTTP_AUTH_PASSWORDS": "password,wordpass,passkey",
|
||||
"SITE_HTTP_AUTH_USERNAMES": "admin,tester,contributor",
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
|
||||
"SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
|
||||
"SLACK_URL": "https://hooks.slack.com/services/some-url",
|
||||
"SLOW_REQUEST_THRESHOLD": 1000,
|
||||
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
|
||||
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
|
||||
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
|
||||
"TRANSIFEX_SLACK_CHANNEL": "transifex",
|
||||
"WEB_CONCURRENCY": 1,
|
||||
"SKIP_SSL_CHECK_KEY": "key",
|
||||
"ENABLE_STACKDRIVER_TRACING": "false",
|
||||
"APPLE_AUTH_PRIVATE_KEY": "",
|
||||
"APPLE_TEAM_ID": "",
|
||||
"APPLE_AUTH_CLIENT_ID": "",
|
||||
"APPLE_AUTH_KEY_ID": "",
|
||||
"BLOCKED_IPS": "",
|
||||
"LOG_AMPLITUDE_EVENTS": "false",
|
||||
"RATE_LIMITER_ENABLED": "false",
|
||||
"LIVELINESS_PROBE_KEY": "",
|
||||
"REDIS_HOST": "aaabbbcccdddeeefff",
|
||||
"REDIS_PORT": "1234",
|
||||
"REDIS_PASSWORD": "12345678",
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
|
||||
"TIME_TRAVEL_ENABLED": "false",
|
||||
"DEBUG_ENABLED": "false",
|
||||
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
|
||||
"SLOW_REQUEST_THRESHOLD": 1000
|
||||
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
|
||||
"WEB_CONCURRENCY": 1
|
||||
}
|
||||
|
||||
Submodule habitica-images updated: aa72332019...e3215f16f9
@@ -10,7 +10,7 @@ function setUpServer () {
|
||||
|
||||
setupNconf();
|
||||
|
||||
// We require src/server and npt src/index because
|
||||
// We require src/server and not src/index because
|
||||
// 1. nconf is already setup
|
||||
// 2. we don't need clustering
|
||||
require('../website/server/server'); // eslint-disable-line global-require
|
||||
|
||||
@@ -11,7 +11,7 @@ const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
/*
|
||||
* Award users every extant pet and mount
|
||||
* Award every extant piece of equippable gear
|
||||
*/
|
||||
|
||||
async function updateUser (user) {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20181203_take_this';
|
||||
const MIGRATION_NAME = 'YYYYMMDD_take_this';
|
||||
const CHALLENGE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
@@ -41,15 +42,15 @@ async function updateUser (user) {
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
if (push) {
|
||||
return User.update({ _id: user._id }, { $set: set, $push: push }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set, $push: push }).exec();
|
||||
}
|
||||
return User.update({ _id: user._id }, { $set: set }).exec();
|
||||
return User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
challenges: '00708425-d477-41a5-bf27-6270466e7976',
|
||||
challenges: CHALLENGE_ID,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
13443
package-lock.json
generated
13443
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.38.2",
|
||||
"version": "5.41.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,8 +19,8 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
"csv-stringify": "^5.6.5",
|
||||
"cwait": "^1.1.1",
|
||||
@@ -42,6 +42,7 @@
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ip-location-api": "^4.0.0",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -51,10 +52,10 @@
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.0",
|
||||
"morgan": "^1.10.1",
|
||||
"nconf": "^0.12.1",
|
||||
"node-gcm": "^1.0.5",
|
||||
"on-headers": "^1.0.2",
|
||||
"on-headers": "^1.1.0",
|
||||
"passport": "^0.5.3",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
@@ -70,7 +71,6 @@
|
||||
"sinon": "^15.2.0",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
@@ -106,8 +106,8 @@
|
||||
"start": "node --watch ./website/server/index.js",
|
||||
"start:simple": "node ./website/server/index.js",
|
||||
"debug": "node --watch --inspect ./website/server/index.js",
|
||||
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2404 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data --number 1 --quiet",
|
||||
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data-testing --number 1 --quiet",
|
||||
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
|
||||
"apidoc": "gulp apidoc",
|
||||
"heroku-postbuild": ".heroku/report_deploy.sh"
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import { Visitor } from 'universal-analytics';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
|
||||
sandbox.stub(Visitor.prototype, 'event');
|
||||
sandbox.stub(Visitor.prototype, 'transaction');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -37,8 +33,6 @@ describe('analyticsService', () => {
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
@@ -49,6 +43,11 @@ describe('analyticsService', () => {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -295,6 +294,9 @@ describe('analyticsService', () => {
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
@@ -326,37 +328,12 @@ describe('analyticsService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'Cron',
|
||||
ec: 'behavior',
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data; let
|
||||
itemSpy;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
Visitor.prototype.event.yields();
|
||||
|
||||
itemSpy = sandbox.stub().returnsThis();
|
||||
|
||||
Visitor.prototype.transaction.returns({
|
||||
item: itemSpy,
|
||||
send: sandbox.stub().yields(),
|
||||
});
|
||||
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
@@ -370,6 +347,11 @@ describe('analyticsService', () => {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -533,6 +515,9 @@ describe('analyticsService', () => {
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
@@ -561,26 +546,6 @@ describe('analyticsService', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('GA', () => {
|
||||
it('calls out to GA', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledOnce;
|
||||
expect(Visitor.prototype.transaction).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Visitor.prototype.event).to.be.calledWith({
|
||||
ea: 'checkout',
|
||||
ec: 'commerce',
|
||||
el: 'PayPal',
|
||||
ev: 8,
|
||||
});
|
||||
expect(Visitor.prototype.transaction).to.be.calledWith('user-id', 8);
|
||||
expect(itemSpy).to.be.calledWith(8, 1, 'paypal-checkout', 'Gems', 'checkout');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ describe('bug-report', () => {
|
||||
emailData: {
|
||||
BROWSER_UA: userAgent,
|
||||
REPORT_MSG: userMessage,
|
||||
USER_ANALYTICS: undefined,
|
||||
USER_CLASS: 'warrior',
|
||||
USER_CONSECUTIVE_MONTHS: 0,
|
||||
USER_COSTUME: 'false',
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -117,15 +117,6 @@ describe('Blocker middleware', () => {
|
||||
|
||||
checkIPBlockedErrorThrown(next);
|
||||
});
|
||||
|
||||
it('throws when the ip is blocked', () => {
|
||||
req.ip = '192.168.1.1';
|
||||
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
|
||||
const attachBlocker = requireAgain(pathToBlocker).default;
|
||||
attachBlocker(req, res, next);
|
||||
|
||||
checkIPBlockedErrorThrown(next);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Blocking clients', () => {
|
||||
@@ -203,4 +194,4 @@ describe('Blocker middleware', () => {
|
||||
expect(calledWith[0] instanceof Forbidden).to.equal(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
requester,
|
||||
translate as t,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
|
||||
@@ -56,4 +57,28 @@ describe('GET /content', () => {
|
||||
const res = await requester().get('/content?filter=backgroundsFlat,invalid');
|
||||
expect(res).to.not.have.property('backgroundsFlat');
|
||||
});
|
||||
|
||||
describe('authenticated user', () => {
|
||||
let user;
|
||||
it('returns content in user\'s preferred language when no language parameter is provided', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'de' });
|
||||
const res = await user.get('/content');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de'));
|
||||
});
|
||||
|
||||
it('respects language parameter over user\'s preferred language', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'de' });
|
||||
const res = await user.get('/content?language=fr');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr'));
|
||||
});
|
||||
|
||||
it('falls back to English if user\'s preferred language is invalid', async () => {
|
||||
user = await generateUser({ 'preferences.language': 'invalid_lang' });
|
||||
const res = await user.get('/content');
|
||||
expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach');
|
||||
expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
it('registers a new user', async () => {
|
||||
const response = await api.get(appleEndpoint);
|
||||
const response = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||
|
||||
expect(response.apiToken).to.exist;
|
||||
expect(response.id).to.exist;
|
||||
@@ -35,7 +35,7 @@ describe('GET /user/auth/apple', () => {
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.get(appleEndpoint);
|
||||
const registerResponse = await api.get(`${appleEndpoint}?allowRegister=true`);
|
||||
|
||||
const response = await api.get(appleEndpoint);
|
||||
|
||||
|
||||
@@ -110,6 +110,18 @@ describe('POST /user/auth/local/login', () => {
|
||||
expect(isValidPassword).to.equal(true);
|
||||
});
|
||||
|
||||
it('sets auth.timestamps.updated', async () => {
|
||||
const oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
// login
|
||||
await api.post(endpoint, {
|
||||
username: user.auth.local.email,
|
||||
password,
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
});
|
||||
|
||||
it('user uses social authentication and has no password', async () => {
|
||||
await user.unset({
|
||||
'auth.local.hashed_password': 1,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
translate as t,
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
@@ -64,6 +65,18 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('fails if allowRegister is false and user does not exist', async () => {
|
||||
await expect(api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: `${apiErrorMessages.socialFlowUserNotFound} ${user.auth.local.username}+google@example.com`,
|
||||
});
|
||||
});
|
||||
|
||||
it('logs an existing user in', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -131,6 +144,36 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('logs an existing user into their social account if allowRegister is false', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
expect(registerResponse.newUser).to.be.true;
|
||||
// This is important for existing accounts before the new social handling
|
||||
passport._strategies.google.userProfile.restore();
|
||||
const expectedResult = {
|
||||
id: randomGoogleId,
|
||||
displayName: 'a google user',
|
||||
emails: [
|
||||
{ value: user.auth.local.email },
|
||||
],
|
||||
};
|
||||
sandbox.stub(passport._strategies.google, 'userProfile').yields(null, expectedResult);
|
||||
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
allowRegister: false,
|
||||
});
|
||||
|
||||
expect(response.apiToken).to.eql(registerResponse.apiToken);
|
||||
expect(response.id).to.eql(registerResponse.id);
|
||||
expect(response.apiToken).not.to.eql(user.apiToken);
|
||||
expect(response.id).not.to.eql(user._id);
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('add social auth to an existing user', async () => {
|
||||
const response = await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -167,5 +210,24 @@ describe('POST /user/auth/social', () => {
|
||||
|
||||
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
|
||||
});
|
||||
|
||||
it('sets auth.timestamps.updated', async () => {
|
||||
let oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
oldUpdated = new Date(user.auth.timestamps.updated);
|
||||
|
||||
// Do it again to ensure it updates even when nothing else changes
|
||||
await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
await user.sync();
|
||||
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
66
test/api/v4/user/auth/POST-check_email.test.js
Normal file
66
test/api/v4/user/auth/POST-check_email.test.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
translate as t,
|
||||
requester,
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v4';
|
||||
|
||||
const ENDPOINT = '/user/auth/check-email';
|
||||
|
||||
describe('POST /user/auth/check-email', () => {
|
||||
const email = 'SOmE-nEw-emAIl_2@example.net';
|
||||
let api;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = requester();
|
||||
});
|
||||
|
||||
it('returns email if it is not used yet', async () => {
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email,
|
||||
});
|
||||
expect(response.email).to.eql(email);
|
||||
expect(response.valid).to.be.true;
|
||||
});
|
||||
|
||||
it('rejects if email is not provided', async () => {
|
||||
await expect(api.post(ENDPOINT, {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Invalid request parameters.',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if email is already taken', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: user.auth.local.email,
|
||||
});
|
||||
expect(response).to.eql({
|
||||
valid: false,
|
||||
email: user.auth.local.email,
|
||||
error: t('cannotFulfillReq'),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if casing is different', async () => {
|
||||
const user = await generateUser();
|
||||
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: user.auth.local.email.toUpperCase(),
|
||||
});
|
||||
expect(response).to.eql({
|
||||
valid: false,
|
||||
email: user.auth.local.email.toUpperCase(),
|
||||
error: t('cannotFulfillReq'),
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects if email uses restricted domain', async () => {
|
||||
const response = await api.post(ENDPOINT, {
|
||||
email: 'fake@habitica.com',
|
||||
});
|
||||
expect(response.valid).to.be.false;
|
||||
});
|
||||
});
|
||||
195
website/client/package-lock.json
generated
195
website/client/package-lock.json
generated
@@ -12,7 +12,6 @@
|
||||
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.3",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^0.28.0",
|
||||
@@ -23,6 +22,7 @@
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"ga-gtag": "^1.2.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"intro.js": "^7.2.0",
|
||||
@@ -37,7 +37,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",
|
||||
@@ -67,49 +67,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
|
||||
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
|
||||
},
|
||||
"node_modules/@amplitude/types": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
|
||||
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/ua-parser-js": {
|
||||
"version": "0.7.33",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
|
||||
"integrity": "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ua-parser-js"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/faisalman"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
|
||||
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
|
||||
"dependencies": {
|
||||
"@amplitude/types": "^1.10.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
|
||||
@@ -1230,6 +1187,7 @@
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
|
||||
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -3049,19 +3007,6 @@
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/amplitude-js": {
|
||||
"version": "8.21.9",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
|
||||
"integrity": "sha512-d0jJH00wbXu7sxKtVwkdSXtVffjqdUrxuACKlnzP7jU5qt9wriXXMgHifdH5Oq+buKmyF8wKL9S02gAykysURA==",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.4.6",
|
||||
"@amplitude/ua-parser-js": "0.7.33",
|
||||
"@amplitude/utils": "^1.10.2",
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"query-string": "8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
@@ -3350,11 +3295,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/blueimp-md5": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
|
||||
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
|
||||
@@ -3393,9 +3333,10 @@
|
||||
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
|
||||
},
|
||||
"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"
|
||||
@@ -3766,14 +3707,6 @@
|
||||
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz",
|
||||
"integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-eql": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz",
|
||||
@@ -4024,13 +3957,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
|
||||
"integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"get-intrinsic": "^1.2.2",
|
||||
"has-tostringtag": "^1.0.0",
|
||||
"hasown": "^2.0.0"
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -4983,17 +4918,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/filter-obj": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz",
|
||||
"integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
||||
@@ -5081,12 +5005,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5116,6 +5043,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",
|
||||
@@ -5154,6 +5095,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ga-gtag": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz",
|
||||
"integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ=="
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -5364,11 +5310,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
|
||||
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.2"
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -7113,6 +7060,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",
|
||||
@@ -7241,22 +7203,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz",
|
||||
"integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.4.1",
|
||||
"filter-obj": "^5.1.0",
|
||||
"split-on-first": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ramda": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz",
|
||||
@@ -7302,7 +7248,8 @@
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
@@ -7816,17 +7763,6 @@
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/split-on-first": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz",
|
||||
"integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -8515,9 +8451,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",
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@froxz/vite-plugin-s3": "^1.6.0",
|
||||
"@vitejs/plugin-vue2": "^2.3.3",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"assert": "^2.1.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^0.28.0",
|
||||
@@ -27,6 +26,7 @@
|
||||
"eslint-config-habitrpg": "6.2.0",
|
||||
"eslint-plugin-mocha": "5.3.0",
|
||||
"eslint-plugin-vue": "7.20.0",
|
||||
"ga-gtag": "^1.2.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"intro.js": "^7.2.0",
|
||||
@@ -41,7 +41,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",
|
||||
|
||||
@@ -108,7 +108,6 @@
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import snackbars from '@/components/snackbars/notifications';
|
||||
import { LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
|
||||
@@ -150,10 +149,6 @@ export default {
|
||||
this.hideLoadingScreen();
|
||||
}
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
// Load external scripts after the app has been rendered
|
||||
Analytics.load();
|
||||
});
|
||||
|
||||
axios.interceptors.response.use(response => { // Set up Response interceptors
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
@@ -212,13 +207,25 @@ export default {
|
||||
const isBanned = this.checkForBannedUser(error);
|
||||
if (isBanned === true) return null; // eslint-disable-line consistent-return
|
||||
|
||||
// Don't show errors from getting user details. These users have delete their account,
|
||||
// Don't show errors from getting user details. These users have deleted their account,
|
||||
// but their chat message still exists.
|
||||
const configExists = Boolean(error.response) && Boolean(error.response.config);
|
||||
if (configExists && error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// Chat paging should help this, but maybe we can also find another solution..
|
||||
return Promise.resolve(error);
|
||||
if (configExists) {
|
||||
if (error.response.config.method === 'get' && error.response.config.url.indexOf('/api/v4/members/') !== -1) {
|
||||
// @TODO: We resolve the promise because we need our caching to cache this user as tried
|
||||
// Chat paging should help this, but maybe we can also find another solution..
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
// Also, a 404 occurs during routine attempt to log in with social,
|
||||
// when we check for account already existing.
|
||||
if (error.response.config.method === 'post' && (error.response.config.url.indexOf('/api/v4/user/auth/social') !== -1
|
||||
|| error.response.config.url.indexOf('/api/v4/user/auth/apple') !== -1)) {
|
||||
const socialEmail = error.response.data.message.split(': ')[1];
|
||||
if (socialEmail) {
|
||||
window.sessionStorage.setItem('social-email', socialEmail);
|
||||
}
|
||||
return Promise.resolve(error);
|
||||
}
|
||||
}
|
||||
|
||||
const errorData = error.response.data;
|
||||
|
||||
@@ -635,6 +635,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_swamp {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_swamp.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_autumn_tree_tunnel {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_tree_tunnel.png');
|
||||
width: 141px;
|
||||
@@ -810,6 +815,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_castle_keep_with_banners {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_castle_keep_with_banners.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_cemetery_gate {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_cemetery_gate.png');
|
||||
width: 141px;
|
||||
@@ -1546,6 +1556,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_inside_forest_witchs_cottage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_inside_forest_witchs_cottage.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_iridescent_clouds {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_iridescent_clouds.png');
|
||||
width: 141px;
|
||||
@@ -29565,6 +29580,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blackPartyDress {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blackPartyDress.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blacksmithsApron {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blacksmithsApron.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -30000,6 +30025,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softOrangeSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softOrangeSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_softPinkSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softPinkSuit.png');
|
||||
width: 114px;
|
||||
@@ -30200,11 +30230,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blackHairbow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackHairbow.png');
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blackSpookySorceryHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackSpookySorceryHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blacksmithsGoggles {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blacksmithsGoggles.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_blueFloppyHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blueFloppyHat.png');
|
||||
width: 90px;
|
||||
@@ -30315,6 +30355,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_floppyOrangeHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyOrangeHat.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_flutteryWig {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
|
||||
width: 114px;
|
||||
@@ -31010,6 +31055,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softOrangePillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softOrangePillow.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_softPinkPillow {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softPinkPillow.png');
|
||||
width: 114px;
|
||||
@@ -31150,6 +31200,16 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blackPartyDress {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blackPartyDress.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blacksmithsApron {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blacksmithsApron.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_blueMoonShozoku {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blueMoonShozoku.png');
|
||||
width: 114px;
|
||||
@@ -31585,6 +31645,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softOrangeSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softOrangeSuit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_softPinkSuit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softPinkSuit.png');
|
||||
width: 114px;
|
||||
@@ -31735,6 +31800,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_blacksmithsHammer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blacksmithsHammer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_blueKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blueKite.png');
|
||||
width: 114px;
|
||||
@@ -33025,6 +33095,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33255,6 +33345,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33425,6 +33535,21 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33640,6 +33765,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -33860,6 +34005,26 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Mage.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Rogue.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fall2025Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_fallHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
|
||||
width: 90px;
|
||||
@@ -35665,6 +35830,41 @@
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.back_mystery_202510 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202510.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.body_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.broad_armor_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.eyewear_mystery_202510 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202510.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202511 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202511.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_mystery_202509 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202509.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_mystery_202511 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202511.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
|
||||
BIN
website/client/src/assets/images/home/signup-quill@2x.png
Normal file
BIN
website/client/src/assets/images/home/signup-quill@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -10,7 +10,7 @@
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
color: $white;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover:not(:disabled):not(.disabled), &:focus {
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
|
||||
&.btn-flat {
|
||||
@@ -28,8 +28,8 @@
|
||||
|
||||
&:disabled, &.disabled {
|
||||
cursor: default;
|
||||
color: $gray-50;
|
||||
opacity: 0.75;
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
box-shadow: none;
|
||||
background-color: $gray-700;
|
||||
border: 2px solid transparent;
|
||||
@@ -164,7 +164,6 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background: $maroon-100;
|
||||
border: 2px solid transparent;
|
||||
@@ -242,29 +241,32 @@
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background: $blue-50;
|
||||
background-color: $blue-100;
|
||||
color: $black;
|
||||
font-weight: 700;
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 2px 0 rgba($black, 0.24);
|
||||
|
||||
&:disabled {
|
||||
background: $blue-50;
|
||||
background-color: $white;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
|
||||
|
||||
&:focus {
|
||||
background: $blue-100;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
color: $black;
|
||||
}
|
||||
|
||||
|
||||
&:hover:not(:disabled):not(.disabled) {
|
||||
background-color: $blue-100;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
background-color: $blue-50;
|
||||
color: $black;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(.disabled), &.active:not(:disabled):not(.disabled) {
|
||||
|
||||
168
website/client/src/assets/scss/forms.scss
Normal file
168
website/client/src/assets/scss/forms.scss
Normal file
@@ -0,0 +1,168 @@
|
||||
// Inputs and textareas
|
||||
|
||||
input, textarea, input.form-control, textarea.form-control {
|
||||
border-radius: 3px;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
padding: 4px 12px;
|
||||
color: $gray-50;
|
||||
border: 1px solid $gray-400;
|
||||
|
||||
&:hover:not(:disabled):not(:read-only):not(:focus):not(:disabled):not(.input-valid):not(.input-invalid):not(.dark) {
|
||||
border-color: $gray-200;
|
||||
}
|
||||
|
||||
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only),
|
||||
&:active:not(:disabled):not(:read-only).dark, &:focus:not(:disabled):not(:read-only).dark {
|
||||
border: 1px solid $purple-400;
|
||||
outline: 1px solid $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 27px;
|
||||
background-image: url(@/assets/svg/for-css/check.svg);
|
||||
background-size: 1rem;
|
||||
border-color: $green-10;
|
||||
}
|
||||
|
||||
&.input-invalid, .input-invalid:hover {
|
||||
padding-right: 40px;
|
||||
background-image: url(@/assets/svg/for-css/alert.svg);
|
||||
background-size: 16px 16px;
|
||||
border-color: $red-100;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $gray-200;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $gray-200;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
.input-invalid.input-with-error {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
|
||||
&:not(.input-valid):not(.input-invalid) {
|
||||
border: 1px solid $purple-300;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $purple-100;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:hover:not(:focus):not(:disabled):not(.input-valid):not(.input-invalid) {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-500;
|
||||
}
|
||||
&:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-500;
|
||||
}
|
||||
&::placeholder { // Standard browsers
|
||||
color: $purple-500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $maroon-500;
|
||||
}
|
||||
|
||||
// checkboxes
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
&:hover~.custom-control-label::before {
|
||||
border-color: $gray-100;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-300;
|
||||
border-color: $purple-300;
|
||||
}
|
||||
|
||||
&:hover:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:disabled)~.custom-control-label::before {
|
||||
background-color: $purple-400;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::after {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url(@/assets/svg/for-css/checkbox-white.svg);
|
||||
background-size: 13px 10px;
|
||||
}
|
||||
|
||||
&:checked:disabled~.custom-control-label::after {
|
||||
background-image: url(@/assets/svg/for-css/checkbox-gray.svg);
|
||||
}
|
||||
|
||||
&:active~.custom-control-label::before {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
~.custom-control-label::before {
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:hover~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-50;
|
||||
}
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $purple-100;
|
||||
border-color: $purple-100;
|
||||
}
|
||||
&:focus~.custom-control-label::before,
|
||||
&:active~.custom-control-label::before {
|
||||
border-color: $purple-400;
|
||||
box-shadow: none;
|
||||
}
|
||||
&:disabled~.custom-control-label::before, &:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: $gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
website/client/src/assets/scss/privacy.scss
Normal file
16
website/client/src/assets/scss/privacy.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.privacy-banner {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
border-radius: 8px;
|
||||
background-color: $white;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
|
||||
width: calc(66vw + 96px);
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
margin: auto 12.5%;
|
||||
}
|
||||
@media only screen and (min-width: 992px) {
|
||||
margin: auto 14.5%;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
li, p {
|
||||
li, p:not(.purple-600) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,10 @@ h4 {
|
||||
background-color: $green-100 !important;
|
||||
}
|
||||
|
||||
.bg-purple-50 {
|
||||
background-color: $purple-50 !important;
|
||||
}
|
||||
|
||||
.bg-purple-100 {
|
||||
background-color: $purple-100 !important;
|
||||
}
|
||||
@@ -119,6 +123,10 @@ h4 {
|
||||
background-color: $purple-300 !important;
|
||||
}
|
||||
|
||||
.bg-yellow-50 {
|
||||
background-color: $yellow-50 !important;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: $white !important;
|
||||
}
|
||||
@@ -131,6 +139,10 @@ h4 {
|
||||
color: $gray-50 !important;
|
||||
}
|
||||
|
||||
.gray-100 {
|
||||
color: $gray-100 !important;
|
||||
}
|
||||
|
||||
.gray-200 {
|
||||
color: $gray-200 !important;
|
||||
}
|
||||
|
||||
3
website/client/src/assets/svg/for-css/checkbox-gray.svg
Normal file
3
website/client/src/assets/svg/for-css/checkbox-gray.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13 10">
|
||||
<path fill="#878190" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.83-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
@@ -1,5 +1,5 @@
|
||||
<svg width="217" height="48" viewBox="0 0 217 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M108.785 0.0195312C106.343 0.0195312 104.355 1.99967 104.355 4.44184C104.355 6.88401 106.343 8.86415 108.785 8.86415C111.227 8.86415 113.215 6.87668 113.215 4.44184C113.215 2.007 111.227 0.0195312 108.785 0.0195312Z" fill="#FF6165"/>
|
||||
<path d="M148.564 0.0195312C146.121 0.0195312 144.134 1.99967 144.134 4.44184C144.134 6.88401 146.121 8.86415 148.564 8.86415C151.006 8.86415 152.993 6.87668 152.993 4.44184C152.993 2.007 151.006 0.0195312 148.564 0.0195312Z" fill="#50B5E9"/>
|
||||
<path d="M184.2 42.1989C181.332 45.8879 177.005 48 172.319 48C164.355 48 157.776 41.8176 157.344 33.9264C157.322 33.5303 157.322 28.8367 157.322 28.7927C157.322 20.5788 164.047 13.8976 172.319 13.8976C176.411 13.8976 180.379 15.5917 183.195 18.54C184.053 19.4347 184.515 20.6154 184.478 21.8548C184.449 23.0943 183.928 24.2457 183.019 25.0964C181.156 26.8565 178.201 26.7759 176.426 24.9277C175.341 23.7983 173.881 23.1749 172.312 23.1749C169.188 23.1749 166.65 25.6904 166.65 28.7853C166.65 29.2694 166.65 32.995 166.665 33.5083C166.841 36.4052 169.327 38.7154 172.312 38.7154C174.087 38.7154 175.722 37.916 176.8 36.5225C178.369 34.4984 181.31 34.1244 183.342 35.6865C184.332 36.4419 184.962 37.5346 185.124 38.7667C185.285 39.9988 184.948 41.2162 184.192 42.1989H184.2ZM216.82 18.4739V43.4164C216.82 45.9392 214.774 47.9927 212.258 47.9927C210.916 47.9927 209.669 47.3986 208.819 46.4159C206.787 47.45 204.543 47.9927 202.262 47.9927C194.533 47.9927 188.145 41.9129 187.727 34.1464C187.705 33.7577 187.705 29.152 187.705 29.1007C187.705 21.0261 194.239 14.4623 202.262 14.4623C204.419 14.4623 206.545 14.9537 208.503 15.8924C209.332 14.6677 210.726 13.8903 212.266 13.8903C214.781 13.8903 216.827 15.9438 216.827 18.4666L216.82 18.4739ZM207.689 33.721C207.697 33.1196 207.704 29.5774 207.704 29.108C207.704 26.0791 205.262 23.6223 202.262 23.6223C199.263 23.6223 196.821 26.0865 196.821 29.108C196.821 29.5701 196.821 33.2443 196.836 33.7577C197.004 36.5812 199.395 38.84 202.262 38.84C205.13 38.84 207.506 36.5959 207.689 33.721ZM63.3042 18.4739V43.4164C63.3042 45.9392 61.2581 47.9927 58.7426 47.9927C57.4006 47.9927 56.1539 47.3986 55.3032 46.4159C53.2717 47.45 51.0276 47.9927 48.7469 47.9927C41.0099 47.9927 34.6296 41.9129 34.2115 34.1464C34.1895 33.8017 34.1895 30.2008 34.1895 29.1007C34.1895 21.0261 40.7238 14.4623 48.7469 14.4623C50.903 14.4623 53.0371 14.9537 54.9878 15.8924C55.8165 14.6677 57.2099 13.8903 58.75 13.8903C61.2654 13.8903 63.3115 15.9438 63.3115 18.4666L63.3042 18.4739ZM48.7469 23.6223C45.7474 23.6223 43.3053 26.0865 43.3053 29.108C43.3053 29.5847 43.3053 33.237 43.32 33.7503C43.4886 36.5812 45.8794 38.84 48.7469 38.84C51.6143 38.84 53.9904 36.5959 54.1738 33.721C54.1811 33.1196 54.1884 29.5847 54.1884 29.108C54.1884 26.0791 51.7463 23.6223 48.7469 23.6223ZM108.78 14.1396C106.338 14.1396 104.351 16.1931 104.351 18.716V43.4164C104.351 45.9392 106.338 47.9927 108.78 47.9927C111.222 47.9927 113.21 45.9392 113.21 43.4164V18.716C113.21 16.1931 111.222 14.1396 108.78 14.1396ZM148.558 14.1396C146.116 14.1396 144.129 16.1931 144.129 18.716V43.4164C144.129 45.9392 146.116 47.9927 148.558 47.9927C151 47.9927 152.988 45.9392 152.988 43.4164V18.716C152.988 16.1931 151 14.1396 148.558 14.1396ZM98.7551 28.866C98.7551 28.91 98.7551 33.5817 98.7331 33.9704C98.3151 41.8396 91.9275 48 84.1978 48C81.917 48 79.6729 47.45 77.6415 46.4012C76.7908 47.3986 75.5441 48 74.1947 48C71.6792 48 69.6331 45.9245 69.6331 43.3797V4.62032C69.6331 2.07548 71.6792 0 74.1947 0C76.7101 0 78.7562 2.07548 78.7562 4.62032V15.1224C80.487 14.411 82.3351 14.037 84.1978 14.037C92.2282 14.037 98.7551 20.6888 98.7551 28.866ZM84.1978 23.285C81.1983 23.285 78.7562 25.7858 78.7562 28.866C78.7562 29.35 78.7562 33.0536 78.7709 33.5743C78.9469 36.4565 81.3303 38.752 84.1978 38.752C87.0653 38.752 89.434 36.4712 89.6247 33.5523C89.6247 32.929 89.6394 29.328 89.6394 28.866C89.6394 25.7858 87.1973 23.285 84.1978 23.285ZM14.3887 14.037C12.6139 14.037 10.8612 14.3597 9.21109 14.9757V4.62766C9.21109 2.08281 7.14299 0.00733401 4.60554 0.00733401C2.06809 0.00733401 0 2.08281 0 4.62766V43.3797C0 45.9319 2.06809 48 4.60554 48C7.14299 48 9.21109 45.9245 9.21109 43.3797V26.5412C9.40176 26.3358 11.1178 23.285 14.3887 23.285C17.4395 23.285 19.9182 25.7858 19.9182 28.866C19.9182 29.482 19.9182 42.529 19.9036 43.2623C19.8376 45.7852 21.759 47.868 24.2524 47.9927C24.3404 47.9927 24.4211 47.9927 24.5091 47.9927C26.9586 47.9927 28.9753 46.0639 29.1 43.607C29.122 43.2257 29.122 29.0053 29.122 28.866C29.122 20.6888 22.5144 14.037 14.3887 14.037ZM136.399 14.037H133.7V4.62032C133.7 2.07548 131.61 0 129.036 0C126.462 0 124.372 2.07548 124.372 4.62032V14.037H121.673C119.106 14.037 117.009 16.1125 117.009 18.6646C117.009 21.2168 119.099 23.285 121.673 23.285H124.372V43.3797C124.372 45.9319 126.462 48 129.036 48C131.61 48 133.7 45.9245 133.7 43.3797V23.285H136.399C138.973 23.285 141.063 21.2095 141.063 18.6646C141.063 16.1198 138.973 14.037 136.399 14.037Z" fill="white"/>
|
||||
<svg width="145" height="32" viewBox="0 0 145 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M72.7484 0.0131836C71.1152 0.0131836 69.7861 1.33328 69.7861 2.96139C69.7861 4.5895 71.1152 5.90959 72.7484 5.90959C74.3816 5.90959 75.7106 4.58461 75.7106 2.96139C75.7106 1.33816 74.3816 0.0131836 72.7484 0.0131836Z" fill="#FF6165"/>
|
||||
<path d="M99.3498 0.0131836C97.7166 0.0131836 96.3875 1.33328 96.3875 2.96139C96.3875 4.5895 97.7166 5.90959 99.3498 5.90959C100.983 5.90959 102.312 4.58461 102.312 2.96139C102.312 1.33816 100.983 0.0131836 99.3498 0.0131836Z" fill="#50B5E9"/>
|
||||
<path d="M123.181 28.1326C121.263 30.5919 118.37 32 115.236 32C109.91 32 105.511 27.8784 105.221 22.6176C105.207 22.3536 105.207 19.2244 105.207 19.1951C105.207 13.7192 109.704 9.26509 115.236 9.26509C117.973 9.26509 120.626 10.3945 122.509 12.36C123.083 12.9565 123.392 13.7436 123.367 14.5699C123.348 15.3962 122.999 16.1638 122.391 16.7309C121.146 17.9044 119.169 17.8506 117.982 16.6185C117.256 15.8655 116.281 15.45 115.231 15.45C113.142 15.45 111.445 17.127 111.445 19.1902C111.445 19.5129 111.445 21.9966 111.455 22.3389C111.572 24.2701 113.235 25.8102 115.231 25.8102C116.418 25.8102 117.511 25.2773 118.232 24.3484C119.282 22.9989 121.249 22.7496 122.607 23.791C123.269 24.2946 123.691 25.0231 123.799 25.8445C123.907 26.6659 123.681 27.4775 123.176 28.1326H123.181ZM144.995 12.316V28.9442C144.995 30.6261 143.627 31.9951 141.945 31.9951C141.047 31.9951 140.213 31.5991 139.645 30.9439C138.286 31.6333 136.785 31.9951 135.26 31.9951C130.091 31.9951 125.819 27.9419 125.54 22.7642C125.525 22.5051 125.525 19.4347 125.525 19.4005C125.525 14.0174 129.895 9.64156 135.26 9.64156C136.702 9.64156 138.124 9.96914 139.434 10.595C139.988 9.77846 140.92 9.2602 141.95 9.2602C143.632 9.2602 145 10.6292 145 12.3111L144.995 12.316ZM138.889 22.4807C138.894 22.0798 138.899 19.7183 138.899 19.4053C138.899 17.3861 137.266 15.7482 135.26 15.7482C133.254 15.7482 131.621 17.391 131.621 19.4053C131.621 19.7134 131.621 22.1629 131.631 22.5051C131.744 24.3875 133.343 25.8934 135.26 25.8934C137.178 25.8934 138.767 24.3972 138.889 22.4807ZM42.3338 12.316V28.9442C42.3338 30.6261 40.9655 31.9951 39.2833 31.9951C38.3858 31.9951 37.5521 31.5991 36.9832 30.9439C35.6247 31.6333 34.124 31.9951 32.5988 31.9951C27.4247 31.9951 23.158 27.9419 22.8785 22.7642C22.8638 22.5345 22.8638 20.1338 22.8638 19.4005C22.8638 14.0174 27.2335 9.64156 32.5988 9.64156C34.0406 9.64156 35.4678 9.96914 36.7723 10.595C37.3265 9.77846 38.2583 9.2602 39.2882 9.2602C40.9704 9.2602 42.3387 10.6292 42.3387 12.3111L42.3338 12.316ZM32.5988 15.7482C30.5929 15.7482 28.9598 17.391 28.9598 19.4053C28.9598 19.7231 28.9598 22.158 28.9696 22.5002C29.0824 24.3875 30.6812 25.8934 32.5988 25.8934C34.5163 25.8934 36.1053 24.3972 36.2279 22.4807C36.2328 22.0798 36.2377 19.7231 36.2377 19.4053C36.2377 17.3861 34.6046 15.7482 32.5988 15.7482ZM72.7452 9.42643C71.1121 9.42643 69.783 10.7954 69.783 12.4773V28.9442C69.783 30.6261 71.1121 31.9951 72.7452 31.9951C74.3783 31.9951 75.7074 30.6261 75.7074 28.9442V12.4773C75.7074 10.7954 74.3783 9.42643 72.7452 9.42643ZM99.346 9.42643C97.7129 9.42643 96.3838 10.7954 96.3838 12.4773V28.9442C96.3838 30.6261 97.7129 31.9951 99.346 31.9951C100.979 31.9951 102.308 30.6261 102.308 28.9442V12.4773C102.308 10.7954 100.979 9.42643 99.346 9.42643ZM66.041 19.244C66.041 19.2733 66.0411 22.3878 66.0264 22.6469C65.7468 27.893 61.4752 32 56.3061 32C54.7808 32 53.2801 31.6333 51.9216 30.9342C51.3527 31.5991 50.519 32 49.6166 32C47.9344 32 46.5662 30.6164 46.5662 28.9198V3.08021C46.5662 1.38365 47.9344 0 49.6166 0C51.2988 0 52.6671 1.38365 52.6671 3.08021V10.0816C53.8245 9.60734 55.0604 9.35798 56.3061 9.35798C61.6762 9.35798 66.041 13.7925 66.041 19.244ZM56.3061 15.5233C54.3002 15.5233 52.6671 17.1905 52.6671 19.244C52.6671 19.5667 52.6671 22.0358 52.6769 22.3829C52.7946 24.3044 54.3885 25.8347 56.3061 25.8347C58.2236 25.8347 59.8077 24.3141 59.9352 22.3682C59.9352 21.9526 59.945 19.552 59.945 19.244C59.945 17.1905 58.3119 15.5233 56.3061 15.5233ZM9.62221 9.35798C8.43537 9.35798 7.26325 9.57311 6.15978 9.98381V3.0851C6.15978 1.38854 4.77677 0.00488934 3.07989 0.00488934C1.38301 0.00488934 0 1.38854 0 3.0851V28.9198C0 30.6212 1.38301 32 3.07989 32C4.77677 32 6.15978 30.6164 6.15978 28.9198V17.6941C6.28729 17.5572 7.4349 15.5233 9.62221 15.5233C11.6624 15.5233 13.32 17.1905 13.32 19.244C13.32 19.6547 13.32 28.3526 13.3102 28.8416C13.2661 30.5235 14.551 31.912 16.2185 31.9951C16.2773 31.9951 16.3313 31.9951 16.3901 31.9951C18.0281 31.9951 19.3768 30.7092 19.4602 29.0714C19.4749 28.8171 19.4749 19.3369 19.4749 19.244C19.4749 13.7925 15.0561 9.35798 9.62221 9.35798ZM91.2147 9.35798H89.41V3.08021C89.41 1.38365 88.0122 0 86.2908 0C84.5694 0 83.1717 1.38365 83.1717 3.08021V9.35798H81.3669C79.6504 9.35798 78.2478 10.7416 78.2478 12.4431C78.2478 14.1445 79.6455 15.5233 81.3669 15.5233H83.1717V28.9198C83.1717 30.6212 84.5694 32 86.2908 32C88.0122 32 89.41 30.6164 89.41 28.9198V15.5233H91.2147C92.9361 15.5233 94.3339 14.1396 94.3339 12.4431C94.3339 10.7465 92.9361 9.35798 91.2147 9.35798Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.0 KiB |
@@ -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;
|
||||
|
||||
@@ -130,4 +130,4 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<buy-gems-modal v-if="user" />
|
||||
<privacy-modal />
|
||||
<footer>
|
||||
<!-- Product -->
|
||||
<div class="product">
|
||||
@@ -848,6 +849,7 @@ import heart from '@/assets/svg/heart.svg?raw';
|
||||
// components & modals
|
||||
import { mapState } from '@/libs/store';
|
||||
import buyGemsModal from './payments/buyGemsModal.vue';
|
||||
import privacyModal from './settings/privacyModal.vue';
|
||||
import reportBug from '@/mixins/reportBug.js';
|
||||
import { worldStateMixin } from '@/mixins/worldState';
|
||||
|
||||
@@ -864,6 +866,7 @@ if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
|
||||
export default {
|
||||
components: {
|
||||
buyGemsModal,
|
||||
privacyModal,
|
||||
},
|
||||
mixins: [
|
||||
reportBug,
|
||||
|
||||
@@ -140,11 +140,6 @@
|
||||
>
|
||||
{{ $t('passwordConfirmationMatch') }}
|
||||
</div>
|
||||
<small
|
||||
v-once
|
||||
class="form-text"
|
||||
v-html="$t('termsAndAgreement')"
|
||||
></small>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<div id="top-background">
|
||||
<div class="seamless_stars_varied_opacity_repeat"></div>
|
||||
</div>
|
||||
<privacy-banner
|
||||
class="privacy-banner"
|
||||
/>
|
||||
<form
|
||||
v-if="!forgotPassword && !resetPasswordSetNewOne"
|
||||
id="login-form"
|
||||
@@ -10,17 +13,18 @@
|
||||
>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<div
|
||||
class="svg-icon svg habitica-logo"
|
||||
<a
|
||||
href="/static/home"
|
||||
class="svg-icon svg habitica-logo mx-auto mb-4"
|
||||
v-html="icons.habiticaIcon"
|
||||
></div>
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('google')"
|
||||
@click="proceed('google')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
@@ -29,18 +33,16 @@
|
||||
<div
|
||||
class="text"
|
||||
>
|
||||
{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Google'})
|
||||
: $t('loginWithSocial', {social: 'Google'}) }}
|
||||
{{ $t('signUpWithSocial', {social: 'Google'}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<div class="col-12 col-md-12">
|
||||
<div class="form-group">
|
||||
<div>
|
||||
<div
|
||||
class="btn btn-secondary social-button"
|
||||
@click="socialAuth('apple')"
|
||||
@click="proceed('apple')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
@@ -49,43 +51,18 @@
|
||||
<div
|
||||
class="text"
|
||||
>
|
||||
{{ registering
|
||||
? $t('signUpWithSocial', {social: 'Apple'})
|
||||
: $t('loginWithSocial', {social: 'Apple'}) }}
|
||||
{{ $t('signUpWithSocial', {social: 'Apple'}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="strike">
|
||||
<div class="strike mb-3">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="registering"
|
||||
class="form-group"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
for="usernameInput"
|
||||
>{{ $t('username') }}</label>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control input-with-error"
|
||||
type="text"
|
||||
:placeholder="$t('usernamePlaceholder')"
|
||||
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
|
||||
>
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!registering"
|
||||
class="form-group"
|
||||
:class="{ 'mb-2': usernameIssues.length > 0 }"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
@@ -94,11 +71,22 @@
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailOrUsername')"
|
||||
:class="{
|
||||
'input-valid': usernameValid,
|
||||
'input-invalid': usernameInvalid,
|
||||
}"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
<div
|
||||
v-if="registering"
|
||||
class="form-group"
|
||||
@@ -110,13 +98,25 @@
|
||||
<input
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control"
|
||||
class="form-control dark"
|
||||
type="email"
|
||||
:placeholder="$t('emailPlaceholder')"
|
||||
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
|
||||
:class="{
|
||||
'input-invalid input-with-error': emailError,
|
||||
'input-valid': emailValid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div
|
||||
class="form-group"
|
||||
:class="{ 'mt-2': usernameIssues.length > 0 }"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
for="passwordInput"
|
||||
@@ -130,12 +130,12 @@
|
||||
<input
|
||||
id="passwordInput"
|
||||
v-model="password"
|
||||
class="form-control"
|
||||
class="form-control dark"
|
||||
type="password"
|
||||
:placeholder="$t(registering ? 'passwordPlaceholder' : 'password')"
|
||||
:class="{
|
||||
'input-invalid input-with-error': registering && passwordInvalid,
|
||||
'input-valid': registering && passwordValid
|
||||
'input-invalid input-with-error': passwordInvalid,
|
||||
'input-valid': passwordValid
|
||||
}"
|
||||
>
|
||||
<div
|
||||
@@ -144,10 +144,16 @@
|
||||
>
|
||||
{{ $t('minPasswordLength') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="passwordInvalid && !registering"
|
||||
class="input-error"
|
||||
>
|
||||
{{ $t('minPasswordLengthLogin') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="registering"
|
||||
class="form-group"
|
||||
class="form-group mb-4"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
@@ -156,7 +162,7 @@
|
||||
<input
|
||||
id="confirmPasswordInput"
|
||||
v-model="passwordConfirm"
|
||||
class="form-control input-with-error"
|
||||
class="form-control dark input-with-error"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPasswordPlaceholder')"
|
||||
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
|
||||
@@ -167,30 +173,26 @@
|
||||
>
|
||||
{{ $t('passwordConfirmationMatch') }}
|
||||
</div>
|
||||
<small
|
||||
v-once
|
||||
class="form-text"
|
||||
v-html="$t('termsAndAgreement')"
|
||||
></small>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
v-if="registering"
|
||||
id="continue-button"
|
||||
type="submit"
|
||||
class="btn btn-info"
|
||||
:disabled="signupFormInvalid"
|
||||
class="btn btn-info w-100 mb-4"
|
||||
:disabled="!(emailValid && passwordValid && passwordConfirmValid)"
|
||||
>
|
||||
{{ $t('joinHabitica') }}
|
||||
{{ $t('continue') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!registering"
|
||||
v-once
|
||||
type="submit"
|
||||
class="btn btn-info"
|
||||
class="btn btn-info w-100 mb-4"
|
||||
:disabled="!usernameValid || !passwordValid"
|
||||
>
|
||||
{{ $t('login') }}
|
||||
</button>
|
||||
<div class="toggle-links">
|
||||
<div>
|
||||
<router-link
|
||||
v-if="registering"
|
||||
:to="{name: 'login'}"
|
||||
@@ -198,7 +200,7 @@
|
||||
>
|
||||
<a
|
||||
v-once
|
||||
class="toggle-link"
|
||||
class="white"
|
||||
v-html="$t('alreadyHaveAccountLogin')"
|
||||
></a>
|
||||
</router-link>
|
||||
@@ -209,7 +211,7 @@
|
||||
>
|
||||
<a
|
||||
v-once
|
||||
class="toggle-link"
|
||||
class="white"
|
||||
v-html="$t('dontHaveAccountSignup')"
|
||||
></a>
|
||||
</router-link>
|
||||
@@ -221,45 +223,66 @@
|
||||
id="forgot-form"
|
||||
@submit.prevent="handleSubmit"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<div>
|
||||
<div class="svg-icon gryphon"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="svg-icon habitica-logo"
|
||||
<a
|
||||
href="/static/home"
|
||||
class="svg-icon habitica-logo mx-auto mb-4"
|
||||
v-html="icons.habiticaIcon"
|
||||
></div>
|
||||
></a>
|
||||
</div>
|
||||
<div class="header">
|
||||
<h2 v-once>
|
||||
<h2
|
||||
v-once
|
||||
class="text-center"
|
||||
>
|
||||
{{ $t('emailNewPass') }}
|
||||
</h2>
|
||||
<p v-once>
|
||||
<p
|
||||
v-once
|
||||
class="purple-600 text-left"
|
||||
>
|
||||
{{ $t('forgotPasswordSteps') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row text-center">
|
||||
<label
|
||||
v-once
|
||||
for="usernameInput"
|
||||
>{{ $t('emailOrUsername') }}</label>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('emailUsernamePlaceholder')"
|
||||
>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div
|
||||
v-once
|
||||
class="btn btn-info"
|
||||
@click="forgotPasswordLink()"
|
||||
class="form-group"
|
||||
:class="{
|
||||
'mb-2': usernameIssues.length > 0,
|
||||
'mb-4': usernameIssues.length === 0,
|
||||
}"
|
||||
>
|
||||
{{ $t('sendLink') }}
|
||||
<label
|
||||
v-once
|
||||
for="usernameInput"
|
||||
>{{ $t('emailOrUsername') }}</label>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailUsernamePlaceholder')"
|
||||
:class="{
|
||||
'input-valid': usernameValid,
|
||||
'input-invalid': usernameInvalid,
|
||||
}"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
:key="issue"
|
||||
class="input-error mb-2"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="btn btn-info w-100"
|
||||
:disabled="!username || usernameIssues.length > 0"
|
||||
@click="forgotPasswordLink()"
|
||||
>
|
||||
{{ $t('sendLink') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -270,10 +293,11 @@
|
||||
>
|
||||
<div class="text-center">
|
||||
<div>
|
||||
<div
|
||||
class="svg-icon habitica-logo"
|
||||
<a
|
||||
href="/static/home"
|
||||
class="svg-icon habitica-logo mx-auto mb-4"
|
||||
v-html="icons.habiticaIcon"
|
||||
></div>
|
||||
></a>
|
||||
</div>
|
||||
<div class="header">
|
||||
<h2>{{ $t('passwordResetPage') }}</h2>
|
||||
@@ -329,15 +353,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
id="bottom-wrap"
|
||||
:class="`bottom-wrap-${!registering ? 'login' : 'register'}`"
|
||||
>
|
||||
<div id="bottom-background">
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
<div class="midground_foreground_extended2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -354,30 +369,10 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
@media only screen and (min-height: 1080px) {
|
||||
.bottom-wrap-register {
|
||||
margin-top: 6em;
|
||||
position: fixed !important;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-height: 862px) {
|
||||
.bottom-wrap-login {
|
||||
margin-top: 6em;
|
||||
position: fixed !important;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
@import '@/assets/scss/forms.scss';
|
||||
@import '@/assets/scss/privacy.scss';
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
#login-form {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
@@ -388,28 +383,28 @@
|
||||
background-color: $purple-200;
|
||||
background: $purple-200; /* For browsers that do not support gradients */
|
||||
background: linear-gradient(to bottom, #4f2a93, #6133b4); /* Standard syntax */
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-400;
|
||||
color: $purple-500;
|
||||
}
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-400;
|
||||
color: $purple-500;
|
||||
}
|
||||
:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-400;
|
||||
color: $purple-500;
|
||||
}
|
||||
:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-400;
|
||||
color: $purple-500;
|
||||
}
|
||||
::placeholder { // Standard browsers
|
||||
color: $purple-400;
|
||||
color: $purple-500;
|
||||
}
|
||||
|
||||
#login-form, #forgot-form, #reset-password-set-new-one-form {
|
||||
margin: 0 auto;
|
||||
width: 40em;
|
||||
width: 448px;
|
||||
height: 700px;
|
||||
padding-top: 5em;
|
||||
padding-bottom: 4em;
|
||||
position: relative;
|
||||
@@ -417,39 +412,23 @@
|
||||
|
||||
.header {
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.gryphon {
|
||||
background-size: cover;
|
||||
color: $white;
|
||||
height: 69.4px;
|
||||
margin: 0 auto;
|
||||
width: 63.2px;
|
||||
p {
|
||||
line-height: 1.714;
|
||||
}
|
||||
}
|
||||
|
||||
.habitica-logo {
|
||||
width: 175px;
|
||||
height: 64px;
|
||||
margin: 2em auto 0;
|
||||
z-index: 0;
|
||||
width: 145px;
|
||||
}
|
||||
|
||||
label {
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-bottom: 2em;
|
||||
border-radius: 2px;
|
||||
background-color: #432874;
|
||||
border-color: transparent;
|
||||
height: 50px;
|
||||
color: $white;
|
||||
line-height: 1.714;
|
||||
}
|
||||
|
||||
.input-with-error.input-invalid {
|
||||
@@ -497,54 +476,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#bottom-wrap {
|
||||
margin-top: 6em;
|
||||
position: static;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#bottom-background {
|
||||
position: relative;
|
||||
|
||||
.seamless_mountains_demo_repeat {
|
||||
background-image: url('@/assets/images/auth/seamless_mountains_demo.png');
|
||||
background-repeat: repeat-x;
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.midground_foreground_extended2 {
|
||||
background-image: url('@/assets/images/auth/midground_foreground_extended2.png');
|
||||
position: relative;
|
||||
width: 1500px;
|
||||
max-width: 100%;
|
||||
height: 150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-links {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.toggle-link {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #bda8ff !important;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: #fff;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.warning-banner {
|
||||
color: $white;
|
||||
background-color: $maroon-100;
|
||||
@@ -569,14 +504,13 @@
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.strike > span {
|
||||
font-weight: 700;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
line-height: 1.14;
|
||||
line-height: 1.714;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -587,7 +521,7 @@
|
||||
top: 50%;
|
||||
width: 9999px;
|
||||
height: 1px;
|
||||
background: #fff;
|
||||
background: $purple-400;
|
||||
}
|
||||
|
||||
.strike > span:before {
|
||||
@@ -603,26 +537,24 @@
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import exclamation from '@/assets/svg/exclamation.svg?raw';
|
||||
import gryphon from '@/assets/svg/gryphon.svg?raw';
|
||||
import habiticaIcon from '@/assets/svg/logo-horizontal.svg?raw';
|
||||
import habiticaIcon from '@/assets/svg/habitica-logo.svg?raw';
|
||||
import googleIcon from '@/assets/svg/google.svg?raw';
|
||||
import appleIcon from '@/assets/svg/apple_black.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [sanitizeRedirect],
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [accountCreation, notifications, sanitizeRedirect],
|
||||
data () {
|
||||
const data = {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
forgotPassword: false,
|
||||
resetPasswordSetNewOneData: {
|
||||
hasError: null,
|
||||
@@ -633,7 +565,6 @@ export default {
|
||||
|
||||
data.icons = Object.freeze({
|
||||
exclamation,
|
||||
gryphon,
|
||||
habiticaIcon,
|
||||
googleIcon,
|
||||
appleIcon,
|
||||
@@ -654,14 +585,6 @@ export default {
|
||||
}
|
||||
return false;
|
||||
},
|
||||
emailValid () {
|
||||
if (this.email.length < 1) return false;
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length < 1) return false;
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
@@ -670,28 +593,6 @@ export default {
|
||||
if (this.username.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordValid () {
|
||||
if (this.password.length <= 0) return false;
|
||||
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
|
||||
},
|
||||
passwordInvalid () {
|
||||
if (this.password.length <= 0) return false;
|
||||
return this.password.length < MINIMUM_PASSWORD_LENGTH;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return !this.passwordConfirmValid;
|
||||
},
|
||||
signupFormInvalid () {
|
||||
return this.usernameInvalid
|
||||
|| this.emailInvalid
|
||||
|| this.passwordInvalid
|
||||
|| this.passwordConfirmInvalid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
@@ -729,71 +630,11 @@ export default {
|
||||
this.username = this.$route.query.email;
|
||||
}
|
||||
}
|
||||
hello.init({
|
||||
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3 || !this.registering) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
async register () {
|
||||
// @TODO do not use alert
|
||||
if (!this.email) {
|
||||
window.alert(this.$t('missingEmail')); // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password !== this.passwordConfirm) {
|
||||
window.alert(this.$t('passwordConfirmationMatch')); // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: implement language and invite accepting
|
||||
// var url = ApiUrl.get() + "/api/v4/user/auth/local/register";
|
||||
// if (location.search && location.search.indexOf('Invite=') !== -1)
|
||||
// { // matches groupInvite and partyInvite
|
||||
// url += location.search;
|
||||
// }
|
||||
//
|
||||
// if($rootScope.selectedLanguage) {
|
||||
// var toAppend = url.indexOf('?') !== -1 ? '&' : '?';
|
||||
// url = url + toAppend + 'lang=' + $rootScope.selectedLanguage.code;
|
||||
// }
|
||||
|
||||
await this.$store.dispatch('auth:register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
});
|
||||
|
||||
const redirectTo = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
// Same situation in login and socialAuth functions
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
async login () {
|
||||
await this.$store.dispatch('auth:login', {
|
||||
username: this.username,
|
||||
// email: this.email,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
@@ -801,31 +642,6 @@ export default {
|
||||
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
const redirectTo = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
},
|
||||
setTitle () {
|
||||
if (this.resetPasswordSetNewOne) {
|
||||
return;
|
||||
@@ -840,7 +656,7 @@ export default {
|
||||
},
|
||||
handleSubmit () {
|
||||
if (this.registering) {
|
||||
this.register();
|
||||
this.proceed('local');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -896,6 +712,20 @@ export default {
|
||||
this.resetPasswordSetNewOneData.hasError = false;
|
||||
this.$router.push({ name: 'login' });
|
||||
},
|
||||
validateUsername: debounce(function valUsername (username) {
|
||||
const usernameIssues = [];
|
||||
if (username.length > 0 && !isEmail(username)) {
|
||||
if (username.length > 20) {
|
||||
usernameIssues.push(this.$t('usernameIssueLength'));
|
||||
}
|
||||
const invalidCharsRegex = /[^a-z0-9_-]/i;
|
||||
const match = username.match(invalidCharsRegex);
|
||||
if (match !== null && match[0] !== null) {
|
||||
usernameIssues.push(this.$t('usernameIssueInvalidCharacters'));
|
||||
}
|
||||
}
|
||||
this.usernameIssues = usernameIssues;
|
||||
}, 500),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
267
website/client/src/components/auth/registerUsername.vue
Normal file
267
website/client/src/components/auth/registerUsername.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div class="gradient-bg">
|
||||
<div
|
||||
id="privacy-tos"
|
||||
class="w-100"
|
||||
>
|
||||
<privacy-banner
|
||||
class="privacy-banner"
|
||||
/>
|
||||
<div class="d-flex flex-column mx-auto pt-5 w-448px">
|
||||
<img
|
||||
class="mx-auto"
|
||||
src="@/assets/images/home/signup-quill@2x.png"
|
||||
width="120px"
|
||||
>
|
||||
<h1 class="mt-0 mb-3 white mx-auto">
|
||||
{{ $t('whatToCallYou') }}
|
||||
</h1>
|
||||
<form
|
||||
class="form mx-auto"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('username')"
|
||||
:class="{
|
||||
'mb-3': !usernameInvalid,
|
||||
'input-valid': username && usernameValid,
|
||||
'input-invalid mb-2': usernameInvalid,
|
||||
}"
|
||||
>
|
||||
<!-- eslint-disable vue/require-v-for-key -->
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
class="input-error"
|
||||
>
|
||||
<!-- eslint-enable vue/require-v-for-key -->
|
||||
{{ issue }}
|
||||
</div>
|
||||
<p class="purple-600">
|
||||
{{ $t('usernameLimitations') }}
|
||||
</p>
|
||||
<div class="custom-control custom-checkbox mb-4">
|
||||
<input
|
||||
id="privacyTOS"
|
||||
v-model="privacyAccepted"
|
||||
class="custom-control-input dark"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
v-once
|
||||
class="custom-control-label purple-600"
|
||||
for="privacyTOS"
|
||||
v-html="$t('acceptPrivacyTOS')"
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!username || usernameInvalid || !privacyAccepted"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
#privacy-tos {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 448px;
|
||||
background-image: url('@/assets/images/auth/seamless_stars_varied_opacity.png');
|
||||
background-repeat: repeat-x;
|
||||
|
||||
a {
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.privacy-banner a {
|
||||
color: $purple-300;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/privacy.scss';
|
||||
@import '@/assets/scss/forms.scss';
|
||||
|
||||
p.purple-600 {
|
||||
line-height: 1.714;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
border-radius: 2px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.custom-control-input:checked~.custom-control-label::after {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-bg {
|
||||
background: linear-gradient(to bottom, $purple-200, $purple-300);
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.sign-up {
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
.w-448px {
|
||||
width: 448px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
privacyAccepted: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
usernameValid () {
|
||||
if (this.username?.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username?.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
username () {
|
||||
this.validateUsername(this.username || '');
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (!this.email) {
|
||||
return;
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
document.getElementById('usernameInput').focus();
|
||||
},
|
||||
methods: {
|
||||
async register () {
|
||||
if (this.registrationMethod === 'local') {
|
||||
let groupInvite = '';
|
||||
if (this.$route.query && this.$route.query.p) {
|
||||
groupInvite = this.$route.query.p;
|
||||
}
|
||||
|
||||
if (this.$route.query && this.$route.query.groupInvite) {
|
||||
groupInvite = this.$route.query.groupInvite;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('auth:register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
groupInvite,
|
||||
});
|
||||
|
||||
const redirect = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
window.location.href = redirect;
|
||||
} else if (this.registrationMethod === 'apple') {
|
||||
await this.$store.dispatch('auth:appleAuth', {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth: this.authData,
|
||||
username: this.username,
|
||||
});
|
||||
}
|
||||
window.location.href = '/';
|
||||
},
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -321,8 +321,8 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment()
|
||||
.isBetween(event.start, event.end));
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:size="editing ? 'lg' : 'md'"
|
||||
:hide-header="true"
|
||||
:hide-footer="true"
|
||||
:modal-class="{'page-2':modalPage > 1 && !editing}"
|
||||
:modal-class="{'page-2': !editing}"
|
||||
:no-close-on-esc="!editing"
|
||||
:no-close-on-backdrop="!editing"
|
||||
>
|
||||
@@ -13,20 +13,6 @@
|
||||
v-if="editing"
|
||||
@close="close()"
|
||||
/>
|
||||
<div
|
||||
v-if="modalPage === 1 && !editing"
|
||||
class="section row welcome-section"
|
||||
>
|
||||
<div class="col-6 offset-3 text-center">
|
||||
<h3 v-once>
|
||||
{{ $t('welcomeTo') }}
|
||||
</h3>
|
||||
<div
|
||||
class="svg-icon logo"
|
||||
v-html="icons.logoPurple"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
v-if="editing"
|
||||
class="text-center pt-2 mt-4 mb-4"
|
||||
@@ -34,7 +20,6 @@
|
||||
{{ $t('editAvatar') }}
|
||||
</h2>
|
||||
<div
|
||||
v-if="modalPage > 1"
|
||||
class="avatar-section d-flex justify-content-center"
|
||||
:class="{'page-2': modalPage === 2}"
|
||||
>
|
||||
@@ -433,8 +418,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="section d-flex justify-content-center justin-outer-section"
|
||||
:class="{top: modalPage > 1}"
|
||||
class="section d-flex justify-content-center justin-outer-section top"
|
||||
>
|
||||
<div class="justin-section d-flex align-items-center">
|
||||
<div class="featured-label">
|
||||
@@ -459,17 +443,8 @@
|
||||
class="corner-decoration"
|
||||
:style="{bottom: '-2px', left: '-2px'}"
|
||||
></div>
|
||||
<div v-if="modalPage === 1">
|
||||
<p
|
||||
v-once
|
||||
v-html="$t('justinIntroMessage1')"
|
||||
></p>
|
||||
<p v-once>
|
||||
{{ $t('justinIntroMessageUsername') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="modalPage === 2">
|
||||
<p>{{ $t('justinIntroMessageAppearance') }}</p>
|
||||
<p v-html="$t('justinIntroMessage1')"></p>
|
||||
</div>
|
||||
<div v-if="modalPage === 3">
|
||||
<p v-once>
|
||||
@@ -484,25 +459,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="modalPage === 1"
|
||||
class="section mr-5 ml-5 first-page-footer"
|
||||
>
|
||||
<username-form
|
||||
:avatar-intro="true"
|
||||
@usernameConfirmed="modalPage += 1"
|
||||
/>
|
||||
<div
|
||||
class="small text-center"
|
||||
v-html="$t('usernameTOSRequirements')"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!editing && !(modalPage === 1)"
|
||||
v-if="!editing"
|
||||
class="section container footer"
|
||||
>
|
||||
<div class="footer-left">
|
||||
<div
|
||||
v-if="modalPage > 1"
|
||||
v-if="modalPage > 2"
|
||||
class="prev-outer"
|
||||
@click="prev()"
|
||||
>
|
||||
@@ -519,10 +481,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-center text-center circles">
|
||||
<div
|
||||
class="circle"
|
||||
:class="{active: modalPage === 1}"
|
||||
></div>
|
||||
<div
|
||||
class="circle"
|
||||
:class="{active: modalPage === 2}"
|
||||
@@ -991,7 +949,6 @@ import forEach from 'lodash/forEach';
|
||||
import content from '@/../../common/script/content/index';
|
||||
import { mapState } from '@/libs/store';
|
||||
import avatar from './avatar';
|
||||
import usernameForm from './settings/usernameForm';
|
||||
import guide from '@/mixins/guide';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import customizeBanner from './avatarModal/customize-banner';
|
||||
@@ -1025,7 +982,6 @@ export default {
|
||||
extraSettings,
|
||||
hairSettings,
|
||||
skinSettings,
|
||||
usernameForm,
|
||||
Sprite,
|
||||
},
|
||||
mixins: [guide, notifications, avatarEditorUtilities],
|
||||
@@ -1053,7 +1009,7 @@ export default {
|
||||
arrowLeft,
|
||||
close: svgClose,
|
||||
}),
|
||||
modalPage: 1,
|
||||
modalPage: 2,
|
||||
activeTopPage: 'body',
|
||||
activeSubPage: 'size',
|
||||
taskCategories: [],
|
||||
|
||||
@@ -445,7 +445,7 @@ export default {
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
|
||||
126
website/client/src/components/header/banners/privacy.vue
Normal file
126
website/client/src/components/header/banners/privacy.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
id="privacy-banner"
|
||||
class="banner d-flex align-items-center justify-content-between py-3 px-4"
|
||||
>
|
||||
<p
|
||||
class="mr-3 mb-0"
|
||||
v-html="$t('privacyOverview') + ' ' + $t('learnMorePrivacy')"
|
||||
>
|
||||
</p>
|
||||
<div
|
||||
class="navigation d-flex flex-column justify-content-around text-center ml-2"
|
||||
:class="{ static: isStaticPage }"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary mb-2"
|
||||
@click="consent(true)"
|
||||
>
|
||||
{{ $t('acceptAllCookies') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary mb-2"
|
||||
@click="consent(false)"
|
||||
>
|
||||
{{ $t('denyNonEssentialCookies') }}
|
||||
</button>
|
||||
<a
|
||||
v-if="isStaticPage"
|
||||
@click="showPrivacyModal"
|
||||
>
|
||||
{{ $t('managePrivacyPreferences') }}
|
||||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
to="/user/settings/siteData"
|
||||
>
|
||||
{{ $t('managePrivacyPreferences') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
button {
|
||||
width: 558px;
|
||||
}
|
||||
|
||||
a, p {
|
||||
line-height: 1.714;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.banner {
|
||||
flex-direction: column !important;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { nextTick } from 'vue';
|
||||
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
|
||||
import { EVENTS } from '@/libs/events';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
mixins: [GenericUserPreferencesMixin],
|
||||
computed: {
|
||||
isStaticPage () {
|
||||
return this.$route.meta.requiresLogin === false;
|
||||
},
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
if (localStorage.getItem('analyticsConsent') !== null
|
||||
|| this.user?.preferences?.analyticsConsent !== undefined
|
||||
|| navigator.globalPrivacyControl
|
||||
) {
|
||||
this.hidden = true;
|
||||
}
|
||||
this.$root.$on('privacy-complete', () => {
|
||||
this.close();
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.hidden = true;
|
||||
nextTick(() => {
|
||||
this.$root.$emit(EVENTS.BANNER_HEIGHT_UPDATED);
|
||||
});
|
||||
},
|
||||
consent (decision) {
|
||||
if (this.user) {
|
||||
this.user.preferences.analyticsConsent = decision;
|
||||
this.setUserPreference('analyticsConsent');
|
||||
} else {
|
||||
localStorage.setItem('analyticsConsent', decision);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
showPrivacyModal () {
|
||||
this.$root.$emit('bv::show::modal', 'privacy-preferences');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -120,7 +120,6 @@
|
||||
<div
|
||||
slot="drawer-slider"
|
||||
class="equipment items items-one-line"
|
||||
:class="getContainerClass()"
|
||||
>
|
||||
<item
|
||||
v-for="(label, group) in gearTypesToStrings"
|
||||
@@ -239,86 +238,6 @@
|
||||
background: $gray-10 !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.equipment.items.items-one-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 0 8px;
|
||||
|
||||
.item-wrapper {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.equipment-scale-default .item-wrapper {
|
||||
.item {
|
||||
width: 94px;
|
||||
height: 92px;
|
||||
}
|
||||
.item-label {
|
||||
width: 94px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.equipment-scale-small .item-wrapper {
|
||||
.item {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
.item-label {
|
||||
width: 70px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrapper:nth-child(4n+1) {
|
||||
clear: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) and (max-width: 1024px) {
|
||||
.equipment.items.items-one-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
|
||||
.item-wrapper {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.equipment-scale-default .item-wrapper {
|
||||
.item {
|
||||
width: 84px;
|
||||
height: 82px;
|
||||
}
|
||||
.item-label {
|
||||
width: 84px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
&.equipment-scale-small .item-wrapper {
|
||||
.item {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
}
|
||||
.item-label {
|
||||
width: 65px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -432,7 +351,6 @@ export default {
|
||||
searchText: null,
|
||||
searchTextThrottled: null,
|
||||
costumeMode: false,
|
||||
windowWidth: window.innerWidth,
|
||||
groupByItems: [
|
||||
'type', 'class',
|
||||
],
|
||||
@@ -605,27 +523,8 @@ export default {
|
||||
subSection: this.$t('equipment'),
|
||||
section: this.$t('inventory'),
|
||||
});
|
||||
|
||||
this.handleResize = throttle(() => {
|
||||
this.windowWidth = window.innerWidth;
|
||||
}, 250);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
beforeDestroy () {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
methods: {
|
||||
getContainerClass () {
|
||||
const equippedCount = Object.keys(this.gearTypesToStrings).filter(group => {
|
||||
const item = this.flatGear[this.activeItems[group]];
|
||||
return item && item.key.indexOf('_base_0') === -1;
|
||||
}).length;
|
||||
|
||||
if (this.windowWidth <= 1024) {
|
||||
return equippedCount > 4 ? 'equipment-scale-small' : 'equipment-scale-default';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
selectDrawerTab (tabName) {
|
||||
let tabNameValue;
|
||||
if (tabName === 'costume') {
|
||||
|
||||
@@ -546,7 +546,7 @@ export default {
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.upgradedGroup.demographics,
|
||||
type: this.paymentData.group.type,
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
}
|
||||
this.paymentData = {};
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
|
||||
122
website/client/src/components/settings/privacyModal.vue
Normal file
122
website/client/src/components/settings/privacyModal.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="privacy-preferences"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
>
|
||||
<close-x
|
||||
@close="close()"
|
||||
/>
|
||||
<h1 class="purple-200 mb-3">
|
||||
{{ $t('yourPrivacyPreferences') }}
|
||||
</h1>
|
||||
<p>
|
||||
{{ $t('privacySettingsOverview') }}
|
||||
</p>
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center mb-1"
|
||||
>
|
||||
<label class="mb-0">
|
||||
{{ $t('performanceAnalytics') }}
|
||||
</label>
|
||||
<toggle-switch
|
||||
v-model="privacyConsent"
|
||||
/>
|
||||
</div>
|
||||
<small>
|
||||
{{ $t('usedForSupport') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="mt-1 mb-4">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center mb-1"
|
||||
>
|
||||
<label class="mb-0">
|
||||
{{ $t('strictlyNecessary') }}
|
||||
</label>
|
||||
<toggle-switch
|
||||
:checked="true"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<small>
|
||||
{{ $t('requiredToRun') }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="d-flex flex-column text-center">
|
||||
<button
|
||||
class="btn btn-primary mb-2"
|
||||
@click="consent(true)"
|
||||
>
|
||||
{{ $t('acceptAllCookies') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary mb-2"
|
||||
@click="consent(false)"
|
||||
>
|
||||
{{ $t('denyNonEssentialCookies') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary mb-3"
|
||||
@click="consent(privacyConsent)"
|
||||
>
|
||||
{{ $t('savePreferences') }}
|
||||
</button>
|
||||
<a
|
||||
href="/static/privacy"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('habiticaPrivacyPolicy') }}
|
||||
</a>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#privacy-preferences {
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
}
|
||||
.modal-content {
|
||||
width: 448px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
label {
|
||||
font-weight: 700;
|
||||
}
|
||||
label, p {
|
||||
line-height: 1.714;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeX from '@/components/ui/closeX';
|
||||
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
closeX,
|
||||
ToggleSwitch,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
privacyConsent: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
consent (decision) {
|
||||
localStorage.setItem('analyticsConsent', decision);
|
||||
this.$root.$emit('privacy-complete');
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'privacy-preferences');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -96,6 +96,7 @@ export default {
|
||||
notificationTopY: '0px',
|
||||
preventMultipleWatchExecution: false,
|
||||
eventPromoBannerHeight: null,
|
||||
privacyBannerHeight: null,
|
||||
sleepingBannerHeight: null,
|
||||
warningBannerHeight: null,
|
||||
};
|
||||
@@ -114,6 +115,10 @@ export default {
|
||||
notificationBannerHeight () {
|
||||
let scrollPosToCheck = 56;
|
||||
|
||||
if (this.privacyBannerHeight) {
|
||||
scrollPosToCheck += this.privacyBannerHeight;
|
||||
}
|
||||
|
||||
if (this.warningBannerHeight) {
|
||||
scrollPosToCheck += this.warningBannerHeight;
|
||||
}
|
||||
@@ -164,6 +169,9 @@ export default {
|
||||
window.addEventListener('scroll', this.updateScrollY, {
|
||||
passive: true,
|
||||
});
|
||||
window.addEventListener('resize', this.updateBannerHeightAndScrollY, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.$root.$on(EVENTS.BANNER_HEIGHT_UPDATED, () => {
|
||||
this.updateBannerHeightAndScrollY();
|
||||
@@ -339,6 +347,7 @@ export default {
|
||||
|
||||
updateBannerHeightAndScrollY () {
|
||||
this.updateEventBannerHeight();
|
||||
this.privacyBannerHeight = document.getElementById('privacy-banner')?.getBoundingClientRect().height || 0;
|
||||
this.warningBannerHeight = getBannerHeight('chat-warning');
|
||||
this.sleepingBannerHeight = getBannerHeight('damage-paused');
|
||||
this.updateScrollY();
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.static-view p {
|
||||
padding-top: 100px;
|
||||
font-size: 2em
|
||||
font-size: 2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,13 +26,19 @@ export default {
|
||||
async mounted () {
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
const reqParams = { code: urlParams.get('code') };
|
||||
const reqParams = { code: urlParams.get('code'), allowRegister: false };
|
||||
if (urlParams.has('name')) {
|
||||
reqParams.name = urlParams.get('name');
|
||||
window.sessionStorage.setItem('apple-name', reqParams.name);
|
||||
}
|
||||
const response = await this.$store.dispatch('auth:appleAuth', reqParams);
|
||||
if (response.id) {
|
||||
window.sessionStorage.removeItem('apple-token');
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
window.location.href = '/username';
|
||||
}
|
||||
await this.$store.dispatch('auth:appleAuth', reqParams);
|
||||
|
||||
window.location.href = '/';
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -117,18 +117,6 @@
|
||||
</div>
|
||||
<div class="bot-right"></div>
|
||||
</div>
|
||||
<b-modal
|
||||
id="group-plan"
|
||||
title
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
>
|
||||
<div>
|
||||
<h2>{{ $t('letsMakeAccount') }}</h2>
|
||||
<auth-form @authenticate="authenticate()" />
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -315,12 +303,10 @@
|
||||
<script>
|
||||
import { setup as setupPayments } from '@/libs/payments';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import AuthForm from '../auth/authForm.vue';
|
||||
import GroupPlanCreationModal from '../group-plans/groupPlanCreationModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AuthForm,
|
||||
GroupPlanCreationModal,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
@@ -353,13 +339,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
authenticate () {
|
||||
this.$root.$emit('bv::hide::modal', 'group-plan');
|
||||
this.$root.$emit('bv::show::modal', 'create-group');
|
||||
},
|
||||
goToNewGroupPage () {
|
||||
if (this.isStaticPage && !this.user) {
|
||||
this.modalOption = 'static';
|
||||
return this.$root.$emit('bv::show::modal', 'group-plan');
|
||||
return this.$router.push('/register');
|
||||
}
|
||||
if (this.upgradingGroup._id) {
|
||||
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });
|
||||
|
||||
@@ -8,124 +8,117 @@
|
||||
<br />
|
||||
<a href="https://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
|
||||
</noscript>
|
||||
<div
|
||||
id="intro-signup"
|
||||
class="purple-1"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-6">
|
||||
<img
|
||||
src="@/assets/images/home/home-main@3x.png"
|
||||
width="357px"
|
||||
>
|
||||
<h1>{{ $t('motivateYourself') }}</h1>
|
||||
<p class="section-main">
|
||||
{{ $t('timeToGetThingsDone', {userCountInMillions}) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-lg-6">
|
||||
<h3 class="text-center">
|
||||
{{ $t('singUpForFree') }}
|
||||
</h3>
|
||||
<form
|
||||
class="form"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<p class="form-text">
|
||||
{{ $t('usernameLimitations') }}
|
||||
<privacy-banner
|
||||
class="privacy-banner"
|
||||
/>
|
||||
<div class="bg-purple-300 white">
|
||||
<div>
|
||||
<div
|
||||
id="intro-signup"
|
||||
>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="w-33 mr-5 mt-5">
|
||||
<img
|
||||
src="@/assets/images/home/home-main@3x.png"
|
||||
width="357px"
|
||||
>
|
||||
<h1>{{ $t('motivateYourself') }}</h1>
|
||||
<p class="section-main">
|
||||
{{ $t('timeToGetThingsDone', {userCountInMillions}) }}
|
||||
</p>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
class="form-control input-with-error"
|
||||
type="text"
|
||||
:placeholder="$t('username')"
|
||||
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
|
||||
>
|
||||
<!-- eslint-disable vue/require-v-for-key -->
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
class="input-error"
|
||||
>
|
||||
<!-- eslint-enable vue/require-v-for-key -->
|
||||
{{ issue }}
|
||||
</div>
|
||||
<input
|
||||
v-model="email"
|
||||
class="form-control"
|
||||
type="email"
|
||||
:placeholder="$t('email')"
|
||||
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
|
||||
>
|
||||
<input
|
||||
v-model="password"
|
||||
class="form-control input-with-error"
|
||||
type="password"
|
||||
:placeholder="$t('password')"
|
||||
:class="{
|
||||
'input-valid': passwordValid,
|
||||
'input-invalid': passwordInvalid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="passwordInvalid"
|
||||
class="input-error"
|
||||
>
|
||||
{{ $t('minPasswordLength') }}
|
||||
</div>
|
||||
<input
|
||||
v-model="passwordConfirm"
|
||||
class="form-control input-with-error"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPassword')"
|
||||
:class="{
|
||||
'input-invalid': passwordConfirmInvalid,
|
||||
'input-valid': passwordConfirmValid}"
|
||||
>
|
||||
<div
|
||||
v-if="passwordConfirmInvalid"
|
||||
class="input-error"
|
||||
>
|
||||
{{ $t('passwordConfirmationMatch') }}
|
||||
</div>
|
||||
<p
|
||||
v-once
|
||||
class="form-text"
|
||||
v-html="$t('termsAndAgreement')"
|
||||
></p>
|
||||
<button
|
||||
class="btn btn-block btn-info sign-up"
|
||||
:disabled="signupFormInvalid"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('signup') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="strike">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('google')"
|
||||
<div class="w-33 ml-5">
|
||||
<h3 class="text-center">
|
||||
{{ $t('singUpForFree') }}
|
||||
</h3>
|
||||
<form
|
||||
class="form pb-0"
|
||||
@submit.prevent.stop="proceed('local')"
|
||||
>
|
||||
<input
|
||||
v-model="email"
|
||||
class="form-control input-with-error dark"
|
||||
type="email"
|
||||
:placeholder="$t('email')"
|
||||
:class="{
|
||||
'mb-3': !emailError,
|
||||
'input-valid': emailValid,
|
||||
'input-invalid mb-2': emailError,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.googleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="social-button"
|
||||
@click="socialAuth('apple')"
|
||||
>
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
<input
|
||||
v-model="password"
|
||||
class="form-control input-with-error dark"
|
||||
type="password"
|
||||
:placeholder="$t('password')"
|
||||
:class="{
|
||||
'mb-3': !passwordInvalid,
|
||||
'input-valid': passwordValid,
|
||||
'input-invalid mb-2': passwordInvalid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="svg svg-icon social-icon apple-icon color"
|
||||
v-html="icons.appleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Apple'}) }}</span>
|
||||
</button>
|
||||
v-if="passwordInvalid"
|
||||
class="input-error"
|
||||
>
|
||||
{{ $t('minPasswordLength') }}
|
||||
</div>
|
||||
<input
|
||||
v-model="passwordConfirm"
|
||||
class="form-control input-with-error dark"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPassword')"
|
||||
:class="{
|
||||
'mb-3': !passwordConfirmInvalid,
|
||||
'input-invalid mb-2': passwordConfirmInvalid,
|
||||
'input-valid': passwordConfirmValid}"
|
||||
>
|
||||
<div
|
||||
v-if="passwordConfirmInvalid"
|
||||
class="input-error"
|
||||
>
|
||||
{{ $t('passwordConfirmationMatch') }}
|
||||
</div>
|
||||
<button
|
||||
id="continue-button"
|
||||
class="btn btn-block btn-info"
|
||||
:disabled="!(emailValid && passwordValid && passwordConfirmValid)"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('continue') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="strike">
|
||||
<span>{{ $t('or') }}</span>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<button
|
||||
class="social-button"
|
||||
@click="proceed('google')"
|
||||
>
|
||||
<div
|
||||
class="svg-icon social-icon"
|
||||
v-html="icons.googleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="social-button"
|
||||
@click="proceed('apple')"
|
||||
>
|
||||
<div
|
||||
class="svg svg-icon social-icon apple-icon color"
|
||||
v-html="icons.appleIcon"
|
||||
></div>
|
||||
<span>{{ $t('signUpWithSocial', {social: 'Apple'}) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
@@ -135,218 +128,218 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="gamify-life"
|
||||
class="purple-2"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="pixel-horizontal svg-icon"
|
||||
v-html="icons.pixelHorizontal"
|
||||
></div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-md-6 col-lg-6 offset-sm-3 text-center">
|
||||
<h2>{{ $t('gamifyYourLife') }}</h2>
|
||||
<p class="section-main">
|
||||
{{ $t('aboutHabitica') }}
|
||||
</p>
|
||||
id="gamify-life"
|
||||
class="bg-purple-100 white"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="pixel-horizontal svg-icon"
|
||||
v-html="icons.pixelHorizontal"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
class="track-habits"
|
||||
src="@/assets/images/home/track-habits@3x.png"
|
||||
width="354px"
|
||||
height="228px"
|
||||
>
|
||||
<strong>{{ $t('trackYourGoals') }}</strong>
|
||||
<p>{{ $t('trackYourGoalsDesc') }}</p>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-6 col-md-6 col-lg-6 offset-sm-3 text-center">
|
||||
<h2>{{ $t('gamifyYourLife') }}</h2>
|
||||
<p class="section-main">
|
||||
{{ $t('aboutHabitica') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
class="track-habits"
|
||||
src="@/assets/images/home/track-habits@3x.png"
|
||||
width="354px"
|
||||
height="228px"
|
||||
>
|
||||
<strong>{{ $t('trackYourGoals') }}</strong>
|
||||
<p>{{ $t('trackYourGoalsDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
src="@/assets/images/home/earn-rewards@3x.png"
|
||||
width="316px"
|
||||
height="244px"
|
||||
>
|
||||
<strong>{{ $t('earnRewards') }}</strong>
|
||||
<p>{{ $t('earnRewardsDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
src="@/assets/images/home/battle-monsters@3x.png"
|
||||
width="303px"
|
||||
height="244px"
|
||||
>
|
||||
<strong>{{ $t('battleMonsters') }}</strong>
|
||||
<p>{{ $t('battleMonstersDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
src="@/assets/images/home/earn-rewards@3x.png"
|
||||
width="316px"
|
||||
height="244px"
|
||||
>
|
||||
<strong>{{ $t('earnRewards') }}</strong>
|
||||
<p>{{ $t('earnRewardsDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<img
|
||||
src="@/assets/images/home/battle-monsters@3x.png"
|
||||
width="303px"
|
||||
height="244px"
|
||||
>
|
||||
<strong>{{ $t('battleMonsters') }}</strong>
|
||||
<p>{{ $t('battleMonstersDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="spacer svg-icon"
|
||||
v-html="icons.spacer"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="use-cases"
|
||||
class="purple-2"
|
||||
>
|
||||
<div class="container text-center">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>{{ $t('playersUseToImprove') }}</h2>
|
||||
<div
|
||||
class="spacer svg-icon"
|
||||
v-html="icons.spacer"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/health-fitness@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('healthAndFitness') }}</strong>
|
||||
<p>{{ $t('healthAndFitnessDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/school-work@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('schoolAndWork') }}</strong>
|
||||
<p>{{ $t('schoolAndWorkDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/much-more@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('muchmuchMore') }}</strong>
|
||||
<p>{{ $t('muchmuchMoreDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="spacer svg-icon"
|
||||
v-html="icons.spacer"
|
||||
></div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
id="use-cases"
|
||||
class="bg-purple-100 white"
|
||||
>
|
||||
<div class="container text-center">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>{{ $t('playersUseToImprove') }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/health-fitness@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('healthAndFitness') }}</strong>
|
||||
<p>{{ $t('healthAndFitnessDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/school-work@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('schoolAndWork') }}</strong>
|
||||
<p>{{ $t('schoolAndWorkDesc') }}</p>
|
||||
</div>
|
||||
<div class="col-12 col-sm-4">
|
||||
<img
|
||||
src="@/assets/images/home/much-more@3x.png"
|
||||
width="300px"
|
||||
height="300px"
|
||||
>
|
||||
<strong>{{ $t('muchmuchMore') }}</strong>
|
||||
<p>{{ $t('muchmuchMoreDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div
|
||||
class="spacer svg-icon"
|
||||
v-html="icons.spacer"
|
||||
></div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="pixel-horizontal-2 svg-icon"
|
||||
v-html="icons.pixelHorizontal2"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="pixel-horizontal-2 svg-icon"
|
||||
v-html="icons.pixelHorizontal2"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="level-up-anywhere"
|
||||
class="purple-3"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-6">
|
||||
<div class="iphones"></div>
|
||||
id="level-up-anywhere"
|
||||
class="bg-purple-50 white"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 col-lg-6">
|
||||
<div class="iphones"></div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-lg-6 text-column">
|
||||
<h2>{{ $t('levelUpAnywhere') }}</h2>
|
||||
<p>{{ $t('levelUpAnywhereDesc') }}</p>
|
||||
<a
|
||||
class="app svg-icon"
|
||||
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
|
||||
target="_blank"
|
||||
v-html="icons.googlePlay"
|
||||
></a>
|
||||
<a
|
||||
class="app svg-icon"
|
||||
href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8"
|
||||
target="_blank"
|
||||
v-html="icons.iosAppStore"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 col-lg-6 text-column">
|
||||
<h2>{{ $t('levelUpAnywhere') }}</h2>
|
||||
<p>{{ $t('levelUpAnywhereDesc') }}</p>
|
||||
<a
|
||||
class="app svg-icon"
|
||||
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
|
||||
target="_blank"
|
||||
v-html="icons.googlePlay"
|
||||
></a>
|
||||
<a
|
||||
class="app svg-icon"
|
||||
href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8"
|
||||
target="_blank"
|
||||
v-html="icons.iosAppStore"
|
||||
></a>
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="pixel-horizontal-3 svg-icon"
|
||||
v-html="icons.pixelHorizontal3"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div
|
||||
class="pixel-horizontal-3 svg-icon"
|
||||
v-html="icons.pixelHorizontal3"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="call-to-action"
|
||||
class="purple-4"
|
||||
>
|
||||
<div class="container featured">
|
||||
<div class="row text-center">
|
||||
<h3 class="col-12">
|
||||
{{ $t('joinMany', {userCountInMillions}) }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary btn-front join-button"
|
||||
@click="playButtonClick()"
|
||||
>
|
||||
{{ $t('joinToday') }}
|
||||
</button>
|
||||
id="call-to-action"
|
||||
class="purple-4 white"
|
||||
>
|
||||
<div class="container featured">
|
||||
<div class="row text-center">
|
||||
<h3 class="col-12">
|
||||
{{ $t('joinMany', {userCountInMillions}) }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<button
|
||||
class="btn btn-primary btn-front join-button"
|
||||
@click="playButtonClick()"
|
||||
>
|
||||
{{ $t('joinToday') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row featured">
|
||||
<div class="col-12 text-center">
|
||||
<strong>{{ $t('featuredIn') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid featured">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<div
|
||||
class="lifehacker svg-icon"
|
||||
v-html="icons.lifehacker"
|
||||
></div>
|
||||
<div
|
||||
class="thenewyorktimes svg-icon"
|
||||
v-html="icons.thenewyorktimes"
|
||||
></div>
|
||||
<div
|
||||
class="makeuseof svg-icon"
|
||||
v-html="icons.makeuseof"
|
||||
></div>
|
||||
<div
|
||||
class="forbes svg-icon"
|
||||
v-html="icons.forbes"
|
||||
></div>
|
||||
<div
|
||||
class="cnet svg-icon"
|
||||
v-html="icons.cnet"
|
||||
></div>
|
||||
<div
|
||||
class="kickstarter svg-icon"
|
||||
v-html="icons.kickstarter"
|
||||
></div>
|
||||
<div
|
||||
class="fast-company svg-icon"
|
||||
v-html="icons.fastCompany"
|
||||
></div>
|
||||
<div
|
||||
class="discover svg-icon"
|
||||
v-html="icons.discover"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row seamless_stars_varied_opacity_repeat"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row featured">
|
||||
<div class="col-12 text-center">
|
||||
<strong>{{ $t('featuredIn') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid featured">
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<div
|
||||
class="lifehacker svg-icon"
|
||||
v-html="icons.lifehacker"
|
||||
></div>
|
||||
<div
|
||||
class="thenewyorktimes svg-icon"
|
||||
v-html="icons.thenewyorktimes"
|
||||
></div>
|
||||
<div
|
||||
class="makeuseof svg-icon"
|
||||
v-html="icons.makeuseof"
|
||||
></div>
|
||||
<div
|
||||
class="forbes svg-icon"
|
||||
v-html="icons.forbes"
|
||||
></div>
|
||||
<div
|
||||
class="cnet svg-icon"
|
||||
v-html="icons.cnet"
|
||||
></div>
|
||||
<div
|
||||
class="kickstarter svg-icon"
|
||||
v-html="icons.kickstarter"
|
||||
></div>
|
||||
<div
|
||||
class="fast-company svg-icon"
|
||||
v-html="icons.fastCompany"
|
||||
></div>
|
||||
<div
|
||||
class="discover svg-icon"
|
||||
v-html="icons.discover"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row seamless_stars_varied_opacity_repeat"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -354,16 +347,27 @@
|
||||
|
||||
<style lang='scss'>
|
||||
@import '@/assets/scss/static.scss';
|
||||
#front .form-text a {
|
||||
color: $white !important;
|
||||
#front {
|
||||
.form-text a {
|
||||
color: $white !important;
|
||||
}
|
||||
.privacy-banner p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/privacy.scss';
|
||||
@import '@/assets/scss/forms.scss';
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
|
||||
|
||||
.w-33 {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
#front {
|
||||
.container-fluid {
|
||||
margin: 0;
|
||||
@@ -374,22 +378,10 @@
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
|
||||
.purple-1, .purple-2, .purple-3, .purple-4, h1, h2, h3, h4, h5 {
|
||||
.custom-control-label, h1, h2, h3, h4, h5 {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.purple-1 {
|
||||
background-color: $purple-300;
|
||||
}
|
||||
|
||||
.purple-2 {
|
||||
background-color: $purple-100;
|
||||
}
|
||||
|
||||
.purple-3 {
|
||||
background-color: $purple-50;
|
||||
}
|
||||
|
||||
.purple-4 {
|
||||
background-color: $header-dark-background;
|
||||
}
|
||||
@@ -423,18 +415,26 @@
|
||||
color: $header-dark-background;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, button, .strike > span, input {
|
||||
h1, h2, h3, h4, h5, h6, .strike > span {
|
||||
font-family: 'Varela Round', sans-serif;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.seamless_stars_varied_opacity_repeat {
|
||||
background-image: url('@/assets/images/auth/seamless_stars_varied_opacity.png');
|
||||
background-repeat: repeat-x;
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#intro-signup {
|
||||
background-image: url('@/assets/svg/for-css/confetti.svg?raw');
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
@media only screen and (min-width: 992px) {
|
||||
margin-left: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
@@ -458,6 +458,7 @@
|
||||
transition: .5s;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -521,64 +522,6 @@
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-bottom: 1em;
|
||||
border-radius: 2px;
|
||||
background-color: $purple-100;
|
||||
border-color: $purple-100;
|
||||
color: $purple-400;
|
||||
border: solid 2px transparent;
|
||||
transition-timing-function: ease;
|
||||
transition: border .5s, color .5s;
|
||||
}
|
||||
|
||||
.input-invalid.input-with-error {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: solid 2px $purple-400;
|
||||
color: #fff;
|
||||
background-color: $purple-50;
|
||||
}
|
||||
|
||||
input:hover {
|
||||
background-color: $purple-50;
|
||||
}
|
||||
|
||||
.sign-up {
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
padding-top: 11px;
|
||||
padding-bottom: 11px;
|
||||
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-input-placeholder { /* Chrome/Opera/Safari */
|
||||
color: $purple-400;
|
||||
}
|
||||
::-moz-placeholder { /* Firefox 19+ */
|
||||
color: $purple-400;
|
||||
}
|
||||
:-ms-input-placeholder { /* IE 10+ */
|
||||
color: $purple-400;
|
||||
}
|
||||
:-moz-placeholder { /* Firefox 18- */
|
||||
color: $purple-400;
|
||||
}
|
||||
::placeholder { // Standard browsers
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
#gamify-life {
|
||||
@@ -779,20 +722,14 @@
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $white;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import googlePlay from '@/assets/images/home/google-play-badge.svg?raw';
|
||||
import iosAppStore from '@/assets/images/home/ios-app-store.svg?raw';
|
||||
import iphones from '@/assets/images/home/iphones.svg?raw';
|
||||
@@ -813,7 +750,10 @@ import makeuseof from '@/assets/images/home/make-use-of.svg?raw';
|
||||
import thenewyorktimes from '@/assets/images/home/the-new-york-times.svg?raw';
|
||||
|
||||
export default {
|
||||
mixins: [sanitizeRedirect],
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [accountCreation, notifications],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -837,132 +777,17 @@ export default {
|
||||
thenewyorktimes,
|
||||
}),
|
||||
userCountInMillions: 4,
|
||||
username: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
email: '',
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
emailValid () {
|
||||
if (this.email.length < 1) return false;
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length < 1) return false;
|
||||
return !isEmail(this.email);
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordValid () {
|
||||
if (this.password.length <= 0) return false;
|
||||
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
|
||||
},
|
||||
passwordInvalid () {
|
||||
if (this.password.length <= 0) return false;
|
||||
return this.password.length < MINIMUM_PASSWORD_LENGTH;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm === this.password;
|
||||
},
|
||||
passwordConfirmInvalid () {
|
||||
if (this.passwordConfirm.length <= 3) return false;
|
||||
return this.passwordConfirm !== this.password;
|
||||
},
|
||||
signupFormInvalid () {
|
||||
return this.usernameInvalid
|
||||
|| this.emailInvalid
|
||||
|| this.passwordInvalid
|
||||
|| this.passwordConfirmInvalid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
username () {
|
||||
this.validateUsername(this.username);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
fullTitle: 'Habitica - Gamify Your Life',
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: this.username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
// @TODO this is totally duplicate from the registerLogin component
|
||||
async register () {
|
||||
let groupInvite = '';
|
||||
if (this.$route.query && this.$route.query.p) {
|
||||
groupInvite = this.$route.query.p;
|
||||
}
|
||||
|
||||
if (this.$route.query && this.$route.query.groupInvite) {
|
||||
groupInvite = this.$route.query.groupInvite;
|
||||
}
|
||||
|
||||
await this.$store.dispatch('auth:register', {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
groupInvite,
|
||||
});
|
||||
|
||||
const redirect = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
window.location.href = redirect;
|
||||
},
|
||||
playButtonClick () {
|
||||
this.$router.push('/register');
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
async socialAuth (network) {
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<chat-banner />
|
||||
<static-header
|
||||
v-if="showContentWrap"
|
||||
v-if="showContentWrap && !loginFlow"
|
||||
:class="{
|
||||
'home-header': ['home', 'front'].indexOf($route.name) !== -1,
|
||||
'white-header': $route.name === 'plans'
|
||||
@@ -15,17 +15,26 @@
|
||||
<router-view />
|
||||
</div>
|
||||
<div
|
||||
v-if="loginFlow"
|
||||
id="bottom-background"
|
||||
class="bg-purple-300"
|
||||
>
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
<div class="midground_foreground_extended2"></div>
|
||||
</div>
|
||||
<app-footer
|
||||
v-if="showContentWrap"
|
||||
:id="footerId"
|
||||
>
|
||||
<app-footer />
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
v-if="showContentWrap && footerId"
|
||||
id="bottom-wrap"
|
||||
class="purple-4"
|
||||
>
|
||||
<div id="bottom-background">
|
||||
<div
|
||||
v-if="!loginFlow"
|
||||
id="bottom-background"
|
||||
>
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
<div class="midground_foreground_extended2"></div>
|
||||
</div>
|
||||
@@ -263,13 +272,16 @@ export default {
|
||||
StaticHeader,
|
||||
},
|
||||
computed: {
|
||||
showContentWrap () {
|
||||
return this.$route.name !== 'news';
|
||||
},
|
||||
footerId () {
|
||||
if (this.$route.name === 'plans') return null;
|
||||
return 'purple-footer';
|
||||
},
|
||||
loginFlow () {
|
||||
return ['login', 'register', 'username'].indexOf(this.$route.name) !== -1;
|
||||
},
|
||||
showContentWrap () {
|
||||
return this.$route.name !== 'news';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -3,49 +3,130 @@
|
||||
<div class="container-fluid">
|
||||
<h1>Terms of Service</h1>
|
||||
<p class="strong pagemeta">
|
||||
Last Updated: December 14, 2021
|
||||
Last Updated September 1, 2025.
|
||||
</p>
|
||||
<p>Thanks for choosing Habitica!</p>
|
||||
<p>
|
||||
Our Service is provided by HabitRPG, Inc. ("HabitRPG"). By accepting these Terms of Service and our Privacy Policy located at: <a
|
||||
href="https://habitica.com/static/privacy"
|
||||
target="_blank"
|
||||
>https://habitica.com/static/privacy</a> (collectively, the "Agreement"), registering for the Service (as defined below), accessing or using any part of the Service, or otherwise manifesting your assent to the Agreement, you acknowledge that you have read, understood, and agree to be legally bound by the Agreement. If you do not agree to (or cannot comply with) the Agreement, you are not permitted to access or use the Service.
|
||||
>https://habitica.com/static/privacy</a> (collectively, the "Agreement"), using our website, Habitica.com, or our other features or services (collectively, “the Services”), or otherwise manifesting your assent to the Agreement, you acknowledge that you have read, understood, and agree to be legally bound by the Agreement. If you do not agree to (or cannot comply with) the Agreement, you are not permitted to access or use the Service. By accepting or agreeing to this Agreement on behalf of a company or other legal entity, you represent and warrant that you have the authority to bind that company or other legal entity to the Agreement and, in such event, "you" and "your" will refer and apply to that company or other legal entity. You further represent and warrant that your assent to this Agreement constitutes an electronic signature as defined by the Electronic Signatures in Global and National Commerce Act (“E-Sign”) and the Uniform Electronic Transactions Act (“UETA”) and that you have formed, executed, entered into, and accepted the terms of and otherwise authenticated the Agreement and acknowledged and agreed that the Agreement is an electronic record for purposes of E- Sign, UETA, and the Uniform Computer Information Transactions Act and, as such, is completely valid, has legal effect, is enforceable, and is binding on, and non- refutable by, you and/or any entity on whose behalf you are acting.
|
||||
</p>
|
||||
<p>By accepting or agreeing to this Agreement on behalf of a company or other legal entity, you represent and warrant that you have the authority to bind that company or other legal entity to the Agreement and, in such event, "you" and "your" will refer and apply to that company or other legal entity.</p>
|
||||
<p class="strong">
|
||||
THE SECTIONS BELOW TITLED "BINDING ARBITRATION," AND "CLASS ACTION WAIVER" CONTAIN A BINDING ARBITRATION AGREEMENT AND CLASS ACTION WAIVER. THEY AFFECT YOUR LEGAL RIGHTS. PLEASE READ THEM.
|
||||
THE SECTIONS BELOW TITLED "BINDING ARBITRATION' AND "CLASS ACTION WAIVER" CONTAIN A BINDING ARBITRATION AGREEMENT AND CLASS ACTION WAIVER. THEY AFFECT YOUR LEGAL RIGHTS. PLEASE READ THEM CAREFULLY.
|
||||
</p>
|
||||
<h2>Changes to the Terms of Service</h2>
|
||||
<p>These Terms of Service are effective as of the last updated date stated at the top of this page. We may change these Terms of Service from time to time with or without notice to you. By accessing the Service after we make any such changes to this Terms of Service, you are deemed to have accepted such changes. Please be aware that, to the extent permitted by applicable law, our use of the information collected is governed by the Terms of Service in effect at the time we collect the information. Please refer back to this Terms of Service on a regular basis.</p>
|
||||
<p>Our Service allows you to upload, store, send, download, or receive content, including but not limited to information, text, graphics, artwork, or other material ("Content"). You retain ownership of any intellectual property rights that you have in your Content. You hereby grant HabitRPG a worldwide, perpetual, irrevocable, sublicenseable, transferable, assignable, non-exclusive, and royalty-free right and license to use, reproduce, distribute, adapt, modify, translate, create derivative works of, publicly perform, publicly display, digitally perform, make, have made, sell, offer for sale, and import your Content, including all intellectual property rights therein. You represent, warrant, and agree that your Content does not and will not violate any third-party intellectual property, privacy, or other rights, and that you have all right, title and interest in and to your Content required to grant us the license above. We reserve the right at all times, but have no obligation, to delete or refuse to use or distribute any Content on or through the Service, including your Content.</p>
|
||||
<h2>Ideas and Suggestions/Requests</h2>
|
||||
<p>HabitRPG appreciates receiving your ideas, comments, suggestions and requests regarding the Service ("Unsolicited Ideas"). By submitting your Unsolicited Ideas (in any form or medium), you are transferring all your right, title and interest therein exclusively to HabitRPG. As the owner of Unsolicited Ideas, we have unrestricted rights to use, disclose and process the Unsolicited Ideas for any purpose whatsoever without any compensation to you. You also give up any claim that any use, disclosure and processing by us or our licensees of your Unsolicited Ideas violates any of your rights, including moral rights, privacy rights, rights to publicity, proprietary or other rights, and rights to credit for the material or ideas set for therein.</p>
|
||||
<h2>Software in Our Service</h2>
|
||||
<p>When the Service requires or includes downloadable software ("Software"), it may may update automatically on your device once a new version or features become available to you. Some platforms may let you adjust your automatic update settings.</p>
|
||||
<p>HabitRPG hereby grants you a personal, worldwide, royalty-free, non-assignable and non-exclusive license to use the Software provided by HabitRPG as part of the Service. You may not copy, modify, distribute, sell, or lease any part of our Service or included Software you are explicitly allowed to do so by the GPL-3.0 license, or you have our written permission for those parts not covered by the open source license.</p>
|
||||
<p>Third party applications may use one of the permitted logos and signifiers in order to represent their applicability to the Service, but may not claim formal association with and/or impersonate HabitRPG or our staff without prior written consent. Third Party applications, the companies that own or provide them, and their employees and agents, are not authorized to make any promises or representations on our behalf, or change the terms of this Agreement.</p>
|
||||
<p>We allow for personal, non-commercial uses like fanart under Commercial Commons License CC-NC-SA 3.0 terms.</p>
|
||||
<p>Outside the above explicitly allowed use cases, you may not use our trademarks, service marks, trade names, logos, domain names, taglines, or trade dress without a signed written contract with us granting you a license to do so.</p>
|
||||
<h2>Modifying and Termination of Service</h2>
|
||||
<p>HabitRPG reserves the right, in its sole discretion, to add, modify, or remove functionalities or features from the Service, and improve, change and/or update the Service. We may also suspend or terminate the Service at any time, with or without notice to you.</p>
|
||||
<p>You can choose to stop using our Service at any time. We may suspend or cease providing the Service to you at any time, including if we determine in our sole discretion, that:</p>
|
||||
<p>
|
||||
These Terms of Service are effective as of the last updated date stated at the top of this page. We may change these Terms of Service from time to time with or without notice to you. By accessing the Service after we make any such changes to this Terms of Service, you are deemed to have accepted such changes. Please be aware that, to the extent permitted by applicable law, our use of the information collected is governed by the Terms of Service in effect at the time we collect the information. Please refer back to this Terms of Service on a regular basis.
|
||||
</p>
|
||||
<h2>Intellectual Property</h2>
|
||||
<p>
|
||||
Our Services allow you to upload, store, send, download, or receive content, including but not limited to information, text, graphics, artwork, or other material ("Content"). You retain ownership of any intellectual property rights that you had in your Content prior to using it in connection with the Service. You hereby grant HabitRPG a worldwide, perpetual, irrevocable, sublicenseable, transferable, assignable, non-exclusive, and royalty-free right and license to use, reproduce, distribute, adapt, modify, translate, create derivative works of, publicly perform, publicly display, digitally perform, make, have made, sell, offer for sale, and import your Content, including all intellectual property rights therein. You represent, warrant, and agree that your Content does not and will not violate any third-party intellectual property, privacy, or other rights, and that you have all right, title and interest in and to your Content required to grant us the license above. We reserve the right at all times, but have no obligation, to delete or refuse to use or distribute any Content on or through the Service, including your Content.
|
||||
</p>
|
||||
<p>
|
||||
HabitRPG appreciates receiving your ideas, comments, suggestions and requests regarding the Service ("Unsolicited Ideas"). By submitting your Unsolicited Ideas (in any form or medium), you are transferring all your right, title and interest therein exclusively to HabitRPG. As the owner of Unsolicited Ideas, we have unrestricted rights to use, disclose and process the Unsolicited Ideas for any purpose whatsoever without any compensation to you. You also give up any claim that any use, disclosure, and/or processing by us or our licensees of your Unsolicited Ideas violates any of your rights, including moral rights, privacy rights, rights to publicity, proprietary or other rights, and rights to credit for the material or ideas set forth therein.
|
||||
</p>
|
||||
<p>
|
||||
<strong>DMCA Notice</strong>. We respect the intellectual property rights of third parties. We respond to notices of alleged copyright infringement according to the Digital Millennium Copyright Act (“DMCA”) at 17 U.S.C. § 512 et seq. Regardless of whether or not the we believe that we are liable for any copyright infringement for which we are provided notice, our response may include removing or disabling access to material claimed to be the subject of infringing activity and/or terminating an individual’s access to the Service, in our sole discretion and operating within the parameters of the DMCA.
|
||||
</p>
|
||||
<p>
|
||||
If you believe that your work has been copied in a manner that constitutes copyright infringement, please contact us at <a href="mailto:admin@habitica.com">admin@habitica.com</a> with the following information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Your name, address, telephone number, and email address;</li>
|
||||
<li>A description of the copyrighted work that you claim has been infringed;</li>
|
||||
<li>A description of the allegedly infringing material and where it is located on the Service;</li>
|
||||
<li>A statement by you that you have a good faith belief that the disputed use is not authorized by the copyright owner, its agents, or the law;</li>
|
||||
<li>A statement by you under penalty of perjury that your notice is accurate, that you are the copyright owner, or that the copyright holder has authorized you to act on its behalf; and</li>
|
||||
<li>Your written or electronic signature attesting to the above.</li>
|
||||
</ul>
|
||||
<p>
|
||||
If your content has been removed from the Service in response to our receipt of a DMCA Notification as outlined above, and you believe the removal was inappropriate, you may submit a DMCA Counter-Notification by contacting us using the information above with the following information:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Your name, address, telephone number, and email address;</li>
|
||||
<li>A statement that you consent to the jurisdiction of Federal District Court for the judicial district in which your address is located, or if your address is outside of the United States, for any judicial district for which jurisdiction for us would be appropriate, and that you will accept service of process from the person who submitted the DMCA notification or an agent of such person;</li>
|
||||
<li>A description of the material that has been removed or to which access has been disabled and the location at which the material appeared before it was removed or access to it was disabled;</li>
|
||||
<li>A statement by you under penalty of perjury that you have a good faith belief that the material was removed or disabled as a result of mistake or misidentification of the material to be removed or disabled; and</li>
|
||||
<li>Your written or electronic signature attesting to the above.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Software in our Service</h2>
|
||||
<p>
|
||||
When the Service requires or includes downloadable software ("Software"), it may update automatically on your device once a new version or features become available to you. Some platforms may let you adjust your automatic update settings.
|
||||
</p>
|
||||
<p>
|
||||
HabitRPG hereby grants you a non-commercial, worldwide, royalty-free, non-assignable and non-exclusive license to use the Software provided by HabitRPG as part of the Service. You may not copy, modify, distribute, sell, or lease any part of our Service or included Software unless you are explicitly allowed to do so by the GPL-3.0 license, or you have our written permission to do so with respect to those parts not covered by the open-source license.
|
||||
</p>
|
||||
<p>
|
||||
Third party applications may use one of the permitted logos and signifiers in order to represent their compatibility with the Service, but may not claim formal association with and/or impersonate HabitRPG or our staff without prior written consent. Third Party applications, the companies that own or provide them, and their employees and agents, are not authorized to make any promises or representations on our behalf, or change the terms of this Agreement.
|
||||
</p>
|
||||
<p>
|
||||
We allow for personal, non-commercial uses like fanart under Commercial Commons License CC-NC-SA 3.0 terms.
|
||||
</p>
|
||||
<p>
|
||||
Outside the above explicitly allowed use cases, you may not use our trademarks, service marks, trade names, logos, domain names, taglines, or trade dress without a signed written contract with us granting you a license to do so.
|
||||
</p>
|
||||
|
||||
<h2>Modification and Termination of Service</h2>
|
||||
<p>
|
||||
HabitRPG reserves the right, in its sole discretion, to add, modify, or remove functionalities or features from the Service, and improve, change and/or update the Service. We may also suspend or terminate the Service at any time, with or without notice to you.
|
||||
</p>
|
||||
<p>
|
||||
You can choose to stop using our Service at any time. We may suspend or cease providing the Service to you at any time, including if we determine in our sole discretion, that:
|
||||
</p>
|
||||
<ul>
|
||||
<li>You have violated any part of this Agreement, the Privacy Policy, or the Community Guidelines;</li>
|
||||
<li>We have stopped offering the Service in your region; or</li>
|
||||
<li>Doing so would be in the best interests of our community, the Service, or the rights of a third party.</li>
|
||||
</ul>
|
||||
<p>If your account is terminated, you will no longer have access to it, including to any of the associated data or Content. You will not be entitled to any refunds and we will have no liability to you. We also reserve the right to terminate any other accounts you may have created, as well as access to any other HabitRPG Service (also without refunds or liability to you).</p>
|
||||
<p>You understand and agree that using the Service comes with the risk that your account may be terminated or suspended at our discretion and at any time. Please keep this risk in mind and comport yourself appropriately.</p>
|
||||
<p>
|
||||
If your account is terminated, you will no longer have access to it, including to any of the associated data or Content. We also reserve the right to terminate any other accounts you may have created, as well as access to any other HabitRPG Service.
|
||||
</p>
|
||||
<p>
|
||||
You understand and agree that using the Service comes with the risk that your account may be terminated or suspended at our discretion and at any time. Please keep this risk in mind and comport yourself appropriately.
|
||||
</p>
|
||||
|
||||
<h2>API</h2>
|
||||
<p>You may access your Service data via the Application Program Interface ("API"). By using API you are automatically bound by the Agreement.</p>
|
||||
<p>
|
||||
You may access your Service data via the Application Program Interface ("API"). By using API you are automatically bound by the Agreement.
|
||||
</p>
|
||||
|
||||
<h2>Links to Third-Party Sites</h2>
|
||||
<p>
|
||||
The Service may contain links to other web sites (“Linked Sites”). The Linked Sites are not under our control and we are not responsible for the contents of any Linked Site, including, without limitation, any link contained in a Linked Site, or any changes or updates to a Linked Site.
|
||||
</p>
|
||||
<p>
|
||||
By providing these links, we do not endorse, sponsor, or recommend such sites or the materials disseminated by or services provided by them, and are not responsible for the materials, services, or other situations at or related to or from any other site. We are not responsible for webcasting or any other form of transmission received from any Linked Site. We are providing these links to you only as a convenience, and the inclusion of any link does not imply endorsement by us of the Linked Site or any association with its operators. We reserve the right to disable links from any third-party sites to the Service.
|
||||
</p>
|
||||
<p>
|
||||
Please exercise discretion while using the Service. You should be aware that when you are using the Service, you could be directed to other sites that are beyond our control. There are links to other sites from the Service pages that take you outside of the Service. These other sites may send their own cookies to users, collect data, solicit personal information, or contain information that you may find inappropriate or offensive.
|
||||
</p>
|
||||
|
||||
<h2>Links to Third-Party Integrations</h2>
|
||||
<p>
|
||||
We may provide links to third-party integrations. Third-party integrations are websites or platforms that synchronize with our Service to provide you with additional functionality, tools, or services, such as delivering content based on your location.
|
||||
</p>
|
||||
<p>
|
||||
You acknowledge and agree that we are not responsible for the availability of such sites or resources and do not endorse and are not responsible or liable for any content, advertising, goods, services, or other materials on, available through, or provided by such sites or resources.
|
||||
</p>
|
||||
<p>
|
||||
We are not responsible for the privacy or other practices of such sites and cannot guarantee the security of any personal information that you provide to such sites or that such sites collect. We encourage you to review the privacy policies and terms and conditions on those linked sites.
|
||||
</p>
|
||||
|
||||
<h2>Using Our Service</h2>
|
||||
<p>You must follow any policies made available to you within the Service, including but not limited to the Terms of Service, Privacy Policy, and Community Guidelines. You may only use our Service as permitted by law. HabitRPG may investigate and/or suspend or terminate our Service to you at any time if we find your use of our Service violates the Terms and/or any policies.</p>
|
||||
<p>Using our Service does not grant you ownership of any intellectual property rights in our Service or the content you may have access to. You may not use any copyrighted content in our Service unless you obtain permission from the content owner and/or are otherwise permitted by law. The Terms do not grant you the right to use any branding or logos used in our Service. Our Service may display some logos, trademarks, or branding materials that are not the property of HabitRPG. Such content is the sole responsibility of the entity that makes it available.</p>
|
||||
<p>You may not abuse and/or misuse our Service, including but not limited to the following actions:</p>
|
||||
<p>
|
||||
You must follow any policies made available to you within the Service, including but not limited to the Terms of Service, Privacy Policy, and Community Guidelines. You may only use our Service as permitted by law. HabitRPG may investigate and/or suspend or terminate our Service to you at any time if we find your use of our Service violates the Agreement, applicable law, and/or any of our policies.
|
||||
</p>
|
||||
<p>
|
||||
Using our Service does not grant you ownership of any intellectual property rights in our Service or the content you may have access to. You may not use any copyrighted content in our Service unless you obtain permission from the content owner and/or are otherwise permitted by law. The Agreement does not grant you the right to use any branding or logos used in our Service. Our Service may display some logos, trademarks, or branding materials that are not the property of HabitRPG. Such content is the sole property of the entity that makes it available.
|
||||
</p>
|
||||
<p>
|
||||
You may not abuse and/or misuse our Service, including but not limited to the following actions:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Using the Service for any unlawful purposes or activities;</li>
|
||||
<li>Uploading any content to the Service in violation of any applicable law, including but not limited to intellectual property laws and publicity laws;</li>
|
||||
<li>Uploading any content to the Service in violation of any applicable law, including but not limited to intellectual property laws, privacy laws, and publicity laws;</li>
|
||||
<li>Sending unsolicited promotions or advertisements;</li>
|
||||
<li>Accessing or tampering with the Service's server systems;</li>
|
||||
<li>Interfering with or disrupting the access of any user, host, or network;</li>
|
||||
@@ -53,53 +134,113 @@
|
||||
<li>Spamming chat, whether for personal or commercial purposes, by disrupting the flow of conversation with repeated postings;</li>
|
||||
<li>Impersonating any person, business, or entity, including an employee of HabitRPG, or member of the Habitica moderation team, or communicating in any way that makes it appear that the communication originates from Habitica staff or HabitRPG;</li>
|
||||
<li>Transmitting or communicating any content which, in the sole and exclusive discretion of HabitRPG, is deemed offensive, including language that is unlawful, harmful, threatening, abusive, harassing, defamatory, vulgar, obscene, sexually explicit, or racially, ethically, or otherwise objectionable,</li>
|
||||
<li>Participating in any action which, in the sole and exclusive judgment of HabitRPG, defrauds any other user of the Game, including by scamming or social engineering; or</li>
|
||||
<li>Participating in any action which, in the sole and exclusive judgment of HabitRPG, defrauds any other user of the Service, including by scamming or social engineering; or</li>
|
||||
<li>Inducing or encouraging others to violate the Community Guidelines or the Agreement.</li>
|
||||
</ul>
|
||||
<p>HabitRPG, in its sole discretion, will determine what constitutes abuse and/or misuse of our Service.</p>
|
||||
<p>
|
||||
HabitRPG, in its sole discretion, will determine what constitutes abuse and/or misuse of our Service.
|
||||
</p>
|
||||
|
||||
<h2>Premium Service and Payments</h2>
|
||||
<p>You may choose our free Service or paid Service ("Premium") depending on your needs. We do not guarantee when, if ever, Premium features will be available in the free Service. You may upgrade from free Service to Premium at any time by any of the following methods:</p>
|
||||
<p>
|
||||
You may choose our free Service or paid Service ("Premium") depending on your needs. We do not guarantee when, if ever, Premium features will be available in the free Service. You may upgrade from free Service to Premium at any time by any of the following methods:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Web: Selecting the user icon in the top right corner and selecting "Subscription" from the dropdown menu,</li>
|
||||
<li>Web: Clicking the green gem icon in the navigation bar at the top of the screen and following instructions to Subscribe or Buy Gems,</li>
|
||||
<li>Android: Tap the menu icon in the top left corner of the main screen and select "Gems & Subscription". Follow the instructions to make a purchase.</li>
|
||||
<li>iOS: Tap the menu icon in the lower right and select "Gems & Subscriptions". Follow the instructions to make a purchase.</li>
|
||||
<li>Android: Tap the menu icon in the top left corner of the main screen and select “Purchase Gems” or “Subscription”. Follow the instructions to make a purchase.</li>
|
||||
<li>iOS: Tap the menu icon in the lower right and select “Purchase Gems” or “Subscription”. Follow the instructions to make a purchase.</li>
|
||||
</ul>
|
||||
<p>You will be charged the amount shown on Pricing before you can access Premium Service. All prices shown on Pricing are inclusive of any applicable sales taxes, levies, value-added taxes, or duties imposed by taxing authorities, and you are responsible for payment of all such taxes, levies, or duties. We may revise the Pricing at any time and may, from time to time, modify, amend, or supplement our fees and fee-billing methods. Such changes shall be effective upon posting on the Pricing page or elsewhere in the Service. If there is a dispute regarding payment of fees to us, we reserve the right to terminate or suspend your account at our sole discretion.</p>
|
||||
<p>BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:</p>
|
||||
<p>WITHIN THIRTY (30) DAYS OF YOUR PREMIUM PAYMENT DATE AS SHOWN ON YOUR PAYMENT BILL, YOU CAN REQUEST A FULL REFUND BY CONTACTING US AT ADMIN@HABITICA.COM. AFTER THIRTY (30) DAYS OF YOUR PREMIUM PAYMENT DATE, ANY PAYMENT REFUND IS SOLELY SUBJECT TO OUR DISCRETION. THE REFUND SHALL BE YOUR SOLE AND EXCLUSIVE REMEDY.</p>
|
||||
<p>
|
||||
You will be charged the amount shown on Pricing before you can access Premium Service. All prices shown on Pricing are inclusive of any applicable sales taxes, levies, value-added taxes, or duties imposed by taxing authorities, and you are responsible for payment of all such taxes, levies, or duties. We may revise the Pricing at any time and may, from time to time, modify, amend, or supplement our fees and fee-billing methods. Such changes shall be effective upon posting on the Pricing page or elsewhere in the Service. If there is a dispute regarding payment of fees to us, we reserve the right to terminate or suspend your account at our sole discretion.
|
||||
</p>
|
||||
<p>
|
||||
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
|
||||
</p>
|
||||
<p>
|
||||
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
|
||||
</p>
|
||||
<p>
|
||||
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
|
||||
href="https://reportaproblem.apple.com"
|
||||
target="_blank"
|
||||
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUND FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUNDS FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
</p>
|
||||
<h2>Warranty Disclaimer</h2>
|
||||
<p>THE SERVICE AND ANY CONTENT MADE AVAILABLE BY HABITRPG VIA THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT ANY WARRANTIES OF ANY KIND, INCLUDING, WITHOUT LIMITATION, THAT THE SERVICE OR CONTENT WILL OPERATE ERROR-FREE OR THAT THE SERVICE OR CONTENT OR ITS SERVERS ARE FREE OF COMPUTER VIRUSES OR SIMILAR CONTAMINATION OR DESTRUCTIVE FEATURES.</p>
|
||||
<p>WE DISCLAIM ALL WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF TITLE, MERCHANTABILITY, NON-INFRINGEMENT OF THIRD PARTIES' RIGHTS, AND FITNESS FOR PARTICULAR PURPOSE AND ANY WARRANTIES ARISING FROM A COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE.</p>
|
||||
<p>WE RESERVE THE RIGHT TO MAKE CHANGES, CORRECTIONS, AND/OR IMPROVEMENTS TO THE SERVICE OR THE CONTENT AT ANY TIME WITHOUT NOTICE.</p>
|
||||
<p>IN CONNECTION WITH ANY WARRANTY, CONTRACT, OR COMMON LAW TORT CLAIMS: (I) WE AND OUR LICENSORS SHALL NOT BE LIABLE FOR ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES, LOST PROFITS, OR DAMAGES RESULTING FROM LOST DATA OR BUSINESS INTERRUPTION RESULTING FROM THE USE OR INABILITY TO ACCESS AND USE THE SERVICE OR CONTENT POSTED BY HABITPRG, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES; AND (II) ANY DIRECT DAMAGES THAT YOU MAY SUFFER AS A RESULT OF YOUR USE OF THE PLATFORM SHALL BE LIMITED TO THE GREATER OF (I) MONIES YOU HAVE PAID US IN CONNECTION WITH YOUR USE OF THE PLATFORM DURING THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE EVENTS GIVING RISE TO THE CLAIM, OR (II) ONE HUNDRED US DOLLARS ($100).</p>
|
||||
<p>SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES. THEREFORE, SOME OF THE ABOVE LIMITATIONS ON WARRANTIES IN THIS SECTION MAY NOT APPLY TO YOU. NOTHING IN THIS AGREEMENT SHALL AFFECT ANY NON-WAIVABLE STATUTORY RIGHTS THAT APPLY TO YOU.</p>
|
||||
|
||||
<h2>Warranty Disclaimer and Limitation on Liability</h2>
|
||||
<p>
|
||||
THE SERVICE AND ANY CONTENT MADE AVAILABLE BY HABITRPG VIA THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT ANY WARRANTIES OF ANY KIND, INCLUDING, WITHOUT LIMITATION, THAT THE SERVICE OR CONTENT WILL OPERATE ERROR-FREE OR THAT THE SERVICE OR CONTENT OR ITS SERVERS ARE FREE OF COMPUTER VIRUSES OR SIMILAR CONTAMINATION OR DESTRUCTIVE FEATURES.
|
||||
</p>
|
||||
<p>
|
||||
WE DISCLAIM ALL WARRANTIES, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF TITLE, MERCHANTABILITY, NON-INFRINGEMENT OF THIRD PARTIES' RIGHTS, AND FITNESS FOR PARTICULAR PURPOSE AND ANY WARRANTIES ARISING FROM A COURSE OF DEALING, COURSE OF PERFORMANCE, OR USAGE OF TRADE.
|
||||
</p>
|
||||
<p>
|
||||
WE RESERVE THE RIGHT TO MAKE CHANGES, CORRECTIONS, AND/OR IMPROVEMENTS TO THE SERVICE OR THE CONTENT AT ANY TIME WITHOUT NOTICE. IN CONNECTION WITH ANY WARRANTY, CONTRACT, OR COMMON LAW TORT CLAIMS: (I) WE AND OUR LICENSORS SHALL NOT BE LIABLE FOR ANY INCIDENTAL OR CONSEQUENTIAL DAMAGES, LOST PROFITS, OR DAMAGES RESULTING FROM LOST DATA OR BUSINESS INTERRUPTION RESULTING FROM THE USE OR INABILITY TO ACCESS AND USE THE SERVICE OR CONTENT POSTED BY HABITRPG, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES; AND (II) ANY DIRECT DAMAGES THAT YOU MAY SUFFER AS A RESULT OF YOUR USE OF THE PLATFORM SHALL BE LIMITED TO THE GREATER OF (I) MONIES YOU HAVE PAID US IN CONNECTION WITH YOUR USE OF THE PLATFORM DURING THE TWELVE (12) MONTHS IMMEDIATELY PRECEDING THE EVENTS GIVING RISE TO THE CLAIM, OR (II) ONE HUNDRED US DOLLARS ($100).
|
||||
</p>
|
||||
<p>
|
||||
SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF CERTAIN WARRANTIES. THEREFORE, SOME OF THE ABOVE LIMITATIONS ON WARRANTIES IN THIS SECTION MAY NOT APPLY TO YOU. NOTHING IN THIS AGREEMENT SHALL AFFECT ANY NON-WAIVABLE STATUTORY RIGHTS THAT APPLY TO YOU.
|
||||
</p>
|
||||
|
||||
<h2>Indemnification</h2>
|
||||
<p>You agree to defend, indemnify, and hold us and our officers, directors, employees, agents, successors, licensees, licensors, and assigns harmless from and against any damages, liabilities, losses, expenses, claims, actions, and/or demands, including, without limitation, reasonable legal and accounting fees, arising or resulting from: (i) your breach of any of your representations, warranties or other obligations under this Agreement; (ii) your use or misuse of the Service or content posted or made available by HabitRPG; and/or (iii) your violation of any third-party rights, including without limitation any copyright, trademark, property, publicity, or privacy right. We shall provide notice to you of any such claim, suit, or proceeding and shall assist you, at your expense, in defending any such claim, suit, or proceeding. We reserve the right to assume the exclusive defense and control (at your expense) of any matter that is subject to indemnification under this section. In such case, you agree to cooperate with any reasonable requests assisting our defense of such matter.</p>
|
||||
<p>
|
||||
You agree to defend, indemnify, and hold us and our officers, directors, employees, agents, successors, licensees, licensors, and assigns harmless from and against any damages, liabilities, losses, expenses, claims, actions, and/or demands, including, without limitation, reasonable legal and accounting fees, arising or resulting from: (i) your breach of any of your representations, warranties or other obligations under this Agreement; (ii) your use or misuse of the Service or content posted or made available by HabitRPG; and/or (iii) your violation of any third-party rights in connection with your use of the Service, including without limitation any copyright, trademark, property, publicity, or privacy right. We shall provide notice to you of any such claim, suit, or proceeding and shall assist you, at your expense, in defending any such claim, suit, or proceeding. We reserve the right to participate in the defense (at our expense) of any matter that is subject to indemnification under this section. In such case, you agree to cooperate with any reasonable requests assisting our defense of such matter.
|
||||
</p>
|
||||
|
||||
<h2>Electronic Communications Privacy Act Notice (18 U.S.C. 2701-2711)</h2>
|
||||
<p>
|
||||
We make no guaranty of confidentiality or privacy with respect to any communication or information transmitted via the Service. We will not be liable for the privacy of the information, e-mail addresses, registration, and identification information, disk space, communications, confidential or trade-secret information, or any other content transmitted over networks accessed by the Service, or otherwise connected with your use of the Service.
|
||||
</p>
|
||||
|
||||
<h2>Compliance with Applicable Laws</h2>
|
||||
<p>The Service is based in the United States. We make no claims concerning whether the Service or posted content may be downloaded, viewed, or be appropriate for use outside of the United States. If you access the Service or such content from outside of the United States, you do so at your own risk. Whether inside or outside of the United States, you are solely responsible for ensuring compliance with the laws of your specific jurisdiction.</p>
|
||||
<p>
|
||||
The Service is based in the United States. We make no claims concerning whether the Service or posted content may be downloaded, viewed, or be appropriate for use outside of the United States. If you access the Service or such content from outside of the United States, you do so at your own risk. Whether inside or outside of the United States, you are solely responsible for ensuring compliance with the laws of your specific jurisdiction.
|
||||
</p>
|
||||
|
||||
<h2>Binding Arbitration</h2>
|
||||
<p>
|
||||
In the event of a dispute arising under or relating to this Agreement or the Service (each, a "<u>Dispute</u>"), such dispute will be finally and exclusively resolved by binding arbitration governed by the Federal Arbitration Act ("<u>FAA</u>"). Any election to arbitrate, at any time, shall be final and binding on the other party. NEITHER PARTY SHALL HAVE THE RIGHT TO LITIGATE SUCH CLAIM IN COURT OR TO HAVE A JURY TRIAL, EXCEPT EITHER PARTY MAY BRING ITS CLAIM IN ITS LOCAL SMALL CLAIMS COURT, IF PERMITTED BY THAT SMALL CLAIMS COURT RULES AND IF WITHIN SUCH COURT'S JURISDICTION. ARBITRATION IS DIFFERENT FROM COURT, AND DISCOVERY AND APPEAL RIGHTS MAY ALSO BE LIMITED IN ARBITRATION. All disputes will be resolved before a neutral arbitrator selected jointly by the parties, whose decision will be final, except for a limited right of appeal under the FAA. The arbitration shall be commenced and conducted by JAMS pursuant to its then current Comprehensive Arbitration Rules and Procedures and in accordance with the Expedited Procedures in those rules, or, where appropriate, pursuant to JAMS' Streamlined Arbitration Rules and Procedures. All applicable JAMS' rules and procedures are available at the JAMS website <a
|
||||
In the event of a dispute arising under or relating to this Agreement or the Service (each, a "Dispute"), such dispute will be finally and exclusively resolved by binding arbitration governed by the Federal Arbitration Act ("FAA"). Any election to arbitrate, at any time, shall be final and binding on the other party. NEITHER PARTY SHALL HAVE THE RIGHT TO LITIGATE SUCH CLAIM IN COURT OR TO HAVE A JURY TRIAL, EXCEPT EITHER PARTY MAY BRING ITS CLAIM IN ITS LOCAL SMALL CLAIMS COURT, IF PERMITTED BY THAT SMALL CLAIMS COURT RULES AND IF WITHIN SUCH COURT'S JURISDICTION. ARBITRATION IS DIFFERENT FROM COURT, AND DISCOVERY AND APPEAL RIGHTS MAY ALSO BE LIMITED IN ARBITRATION.
|
||||
</p>
|
||||
<p>
|
||||
All disputes will be resolved before a neutral arbitrator selected jointly by the parties, whose decision will be final, except for a limited right of appeal under the FAA. The arbitration shall be commenced and conducted by JAMS pursuant to its then current Comprehensive Arbitration Rules and Procedures and in accordance with the Expedited Procedures in those rules, or, where appropriate, pursuant to JAMS' Streamlined Arbitration Rules and Procedures. All applicable JAMS rules and procedures are available at the JAMS website <a
|
||||
href="https://www.jamsadr.com"
|
||||
target="_blank"
|
||||
>www.jamsadr.com</a>. Each party will be responsible for paying any JAMS filing, administrative, and arbitrator fees in accordance with JAMS rules. Judgment on the arbitrator's award may be entered in any court having jurisdiction. This clause shall not preclude parties from seeking provisional remedies in aid of arbitration from a court of appropriate jurisdiction. The arbitration may be conducted in person, through the submission of documents, by phone, or online. If conducted in person, the arbitration shall take place in the United States county where you reside. The parties may litigate in court to compel arbitration, to stay a proceeding pending arbitration, or to confirm, modify, vacate, or enter judgment on the award entered by the arbitrator. The parties shall cooperate in good faith in the voluntary and informal exchange of all non-privileged documents and other information (including electronically stored information) relevant to the Dispute immediately after commencement of the arbitration. As set forth below, nothing in this Agreement will prevent us from seeking injunctive relief in any court of competent jurisdiction as necessary to protect our proprietary interests.
|
||||
</p>
|
||||
<p>ANY CLAIMS, ACTIONS OR PROCEEDINGS BY YOU MUST BE COMMENCED WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR CLAIM OCCURS. ALL OTHER CLAIMS YOU MAY HAVE ARE PERMANENTLY BARRED.</p>
|
||||
<p>
|
||||
ANY CLAIMS, ACTIONS OR PROCEEDINGS BY YOU MUST BE COMMENCED WITHIN ONE YEAR AFTER THE EVENT THAT GAVE RISE TO YOUR CLAIM OCCURS. ALL OTHER CLAIMS YOU MAY HAVE ARE PERMANENTLY BARRED.
|
||||
</p>
|
||||
|
||||
<h2>Class Action Waiver</h2>
|
||||
<p>You agree that any arbitration or proceeding shall be limited to the Dispute between us and you individually. To the full extent permitted by law, (i) no arbitration or proceeding shall be joined with any other; (ii) there is no right or authority for any Dispute to be arbitrated or resolved on a class action-basis or to utilize class action procedures; and (iii) there is no right or authority for any Dispute to be brought in a purported representative capacity on behalf of the general public or any other persons. YOU AGREE THAT YOU MAY BRING CLAIMS AGAINST US ONLY IN YOUR INDIVIDUAL CAPACITY AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING.</p>
|
||||
<p>
|
||||
You agree that any arbitration or proceeding shall be limited to the Dispute between us and you individually. To the full extent permitted by law, (i) no arbitration or proceeding shall be joined with any other; (ii) there is no right or authority for any Dispute to be arbitrated or resolved on a class action-basis or to utilize class action procedures; and (iii) there is no right or authority for any Dispute to be brought in a purported representative capacity on behalf of the general public or any other persons. YOU AGREE THAT YOU MAY BRING CLAIMS AGAINST US ONLY IN YOUR INDIVIDUAL CAPACITY AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR REPRESENTATIVE PROCEEDING.
|
||||
</p>
|
||||
|
||||
<h2>Jury Trial Waiver</h2>
|
||||
<p>
|
||||
IF FOR ANY REASON A DISPUTE OR CLAIM MAY PROCEED IN COURT RATHER THAN IN ARBITRATION, EACH PARTY TO THIS AGREEMENT IRREVOCABLY WAIVES, TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, ANY RIGHT IT MAY HAVE TO A TRIAL BY JURY IN ANY LEGAL PROCEEDING DIRECTLY OR INDIRECTLY ARISING OUT OF OR RELATING TO THESE TERMS OR THE SERVICE (WHETHER BASED ON CONTRACT, TORT, OR ANY OTHER THEORY), UNLESS SUCH WAIVERS ARE UNENFORCEABLE.
|
||||
</p>
|
||||
|
||||
<h2>Equitable Relief</h2>
|
||||
<p>You acknowledge and agree that in the event of a breach or threatened violation of our intellectual property rights and confidential and proprietary information by you, we will suffer irreparable harm and will therefore be entitled to injunctive relief to enforce this Agreement. We may, without waiving any other remedies under this Agreement, seek from any court having jurisdiction any interim, equitable, provisional, or injunctive relief that is necessary to protect our rights and property pending the outcome of the arbitration referenced above.</p>
|
||||
<p>
|
||||
You acknowledge and agree that in the event of a breach or threatened violation by you of our intellectual property rights and/or rights related to our confidential and proprietary information, we will suffer irreparable harm and will therefore be entitled to injunctive relief to enforce this Agreement. We may, without waiving any other remedies under this Agreement, seek from any court having jurisdiction any interim, equitable, provisional, or injunctive relief that is necessary to protect our rights and property pending the outcome of the arbitration referenced above.
|
||||
</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>If you have any questions about the Agreement, or want to report a violation (including DMCA take-down notices relating to infringement of copyright) please contact us at <a href="mailto:admin@habitica.com">admin@habitica.com</a>.</p>
|
||||
<p>
|
||||
If you have any questions about the Agreement, or want to report a violation, please contact us at <a href="mailto:admin@habitica.com">admin@habitica.com</a>.
|
||||
</p>
|
||||
|
||||
<h2>Miscellaneous</h2>
|
||||
<p>This Agreement and any action related thereto will be governed by the laws of the State of California without regard to its conflict of laws provisions. Our failure to act on or enforce any provision of this Agreement shall not be construed as a waiver of that provision or any other provision therein. No waiver shall be effective against us unless made in writing, and no such waiver shall be construed as a waiver in any other or subsequent instance. Except as expressly agreed by us and you in writing, this Agreement constitutes the entire agreement between you and us with respect to the subject matter, and supersedes all previous or contemporaneous agreements, whether written or oral, between the parties with respect to the subject matter. The section headings are provided merely for convenience and shall not be given any legal import. This Agreement will inure to the benefit of our successors, assigns, licensees, and sublicensees.</p>
|
||||
<ul>
|
||||
<li>This Agreement and any action related thereto will be governed by the laws of the State of California without regard to its conflict of laws provisions.</li>
|
||||
<li>Our failure to act on or enforce any provision of this Agreement shall not be construed as a waiver of that provision or any other provision therein. No waiver shall be effective against us unless made in writing, and no such waiver shall be construed as a waiver in any other or subsequent instance.</li>
|
||||
<li>Except as expressly agreed by us and you in writing, this Agreement constitutes the entire agreement between you and us with respect to the subject matter, and supersedes all previous or contemporaneous agreements, whether written or oral, between the parties with respect to the subject matter.</li>
|
||||
<li>The section headings are provided merely for convenience and shall not be given any legal import. This Agreement will inure to the benefit of our successors, assigns, licensees, and sublicensees.</li>
|
||||
<li>You agree that no joint venture, partnership, employment, or agency relationship exists between you and us as a result of the Agreement or your use of the Service.</li>
|
||||
<li>Our performance under the Agreement is subject to existing laws and legal process, and nothing contained in the Agreement is in derogation of our right to comply with governmental, court, and law enforcement requests, or requirements relating to your use of the Service or information provided to or gathered by us with respect to such use.</li>
|
||||
<li>If any part of the Agreement is determined to be invalid or unenforceable pursuant to applicable law, including, without limitation, the warranty disclaimers and liability limitations set forth above, then the invalid or unenforceable provision will be deemed superseded by a valid, enforceable provision that most closely matches the intent of the original provision and the remainder of the Agreement shall continue in effect.</li>
|
||||
<li>A printed version of the Agreement and of any notice given in electronic form shall be admissible in judicial or administrative proceedings based upon or relating to the Agreement to the same extent and subject to the same conditions as other business documents and records originally generated and maintained in printed form.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- eslint-enable max-len -->
|
||||
</template>
|
||||
|
||||
@@ -86,8 +86,8 @@
|
||||
v-if="taskList.length > 0"
|
||||
ref="tasksList"
|
||||
class="sortable-tasks"
|
||||
:options="{disabled: activeFilter.label === 'scheduled' || !canBeDragged(),
|
||||
scrollSensitivity: 64}"
|
||||
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
|
||||
scrollSensitivity="64"
|
||||
:delay-on-touch-only="true"
|
||||
:delay="100"
|
||||
@update="taskSorted"
|
||||
|
||||
@@ -21,11 +21,9 @@
|
||||
>
|
||||
<draggable
|
||||
v-model="checklist"
|
||||
:options="{
|
||||
handle: '.grippy',
|
||||
filter: '.task-dropdown',
|
||||
disabled: disabled,
|
||||
}"
|
||||
handle=".grippy"
|
||||
filter=".task-dropdown"
|
||||
:disabled="disabled"
|
||||
@update="updateChecklist"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -25,13 +25,18 @@
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
:value="value"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
>
|
||||
<label
|
||||
class="toggle-switch-label"
|
||||
:for="toggleId"
|
||||
:class="{ disabled }"
|
||||
>
|
||||
<span class="toggle-switch-inner"></span>
|
||||
<span
|
||||
class="toggle-switch-inner"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="toggle-switch-switch"
|
||||
tabindex="0"
|
||||
@@ -111,7 +116,7 @@
|
||||
.toggle-switch-inner:before {
|
||||
content: "";
|
||||
padding-left: 10px;
|
||||
background-color: $purple-300;
|
||||
background-color: $green-10;
|
||||
}
|
||||
|
||||
.toggle-switch-inner:after {
|
||||
@@ -121,6 +126,14 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: auto;
|
||||
|
||||
.toggle-switch-inner:before, .toggle-switch-inner:after {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-switch-switch {
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
display: block;
|
||||
@@ -181,6 +194,10 @@ export default {
|
||||
hoverText: {
|
||||
type: String,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import forEach from 'lodash/forEach';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import keys from 'lodash/keys';
|
||||
import pick from 'lodash/pick';
|
||||
import includes from 'lodash/includes';
|
||||
import amplitude from 'amplitude-js';
|
||||
import Vue from 'vue';
|
||||
import getStore from '@/store';
|
||||
|
||||
const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production'; // eslint-disable-line no-process-env
|
||||
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY; // eslint-disable-line no-process-env
|
||||
const GA_ID = import.meta.env.GA_ID; // eslint-disable-line no-process-env
|
||||
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
|
||||
|
||||
const REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction'];
|
||||
const ALLOWED_HIT_TYPES = [
|
||||
'pageview',
|
||||
'screenview',
|
||||
'event',
|
||||
'transaction',
|
||||
'item',
|
||||
'social',
|
||||
'exception',
|
||||
'timing',
|
||||
];
|
||||
function _getConsentedUser () {
|
||||
const store = getStore();
|
||||
const user = store.state.user.data;
|
||||
if (!user?.preferences?.analyticsConsent) {
|
||||
return false;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
function _doesNotHaveRequiredFields (properties) {
|
||||
if (!isEqual(keys(pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) {
|
||||
@@ -34,17 +26,6 @@ function _doesNotHaveRequiredFields (properties) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function _doesNotHaveAllowedHitType (properties) {
|
||||
if (!includes(ALLOWED_HIT_TYPES, properties.hitType)) {
|
||||
// @TODO: Log with Winston?
|
||||
// console.log('Hit type of Analytics event must be one
|
||||
// of the following: ' + JSON.stringify(ALLOWED_HIT_TYPES));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function _gatherUserStats (properties) {
|
||||
const store = getStore();
|
||||
const user = store.state.user.data;
|
||||
@@ -75,71 +56,24 @@ function _gatherUserStats (properties) {
|
||||
if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId;
|
||||
}
|
||||
|
||||
export function setUser () {
|
||||
const store = getStore();
|
||||
const user = store.state.user.data;
|
||||
amplitude.getInstance().setUserId(user._id);
|
||||
window.ga('set', { userId: user._id });
|
||||
}
|
||||
|
||||
export function track (properties, options = {}) {
|
||||
export function track (properties) {
|
||||
const user = _getConsentedUser();
|
||||
if (!user) return;
|
||||
// Use nextTick to avoid blocking the UI
|
||||
Vue.nextTick(() => {
|
||||
if (_doesNotHaveRequiredFields(properties)) return;
|
||||
if (_doesNotHaveAllowedHitType(properties)) return;
|
||||
|
||||
const trackOnClient = options && options.trackOnClient === true;
|
||||
// Track events on the server by default
|
||||
if (trackOnClient === true) {
|
||||
amplitude.getInstance().logEvent(properties.eventAction, properties);
|
||||
window.ga('send', properties);
|
||||
} else {
|
||||
const store = getStore();
|
||||
store.dispatch('analytics:trackEvent', properties);
|
||||
}
|
||||
const store = getStore();
|
||||
store.dispatch('analytics:trackEvent', properties);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUser (properties = {}) {
|
||||
const user = _getConsentedUser();
|
||||
if (!user) return;
|
||||
// Use nextTick to avoid blocking the UI
|
||||
Vue.nextTick(() => {
|
||||
_gatherUserStats(properties);
|
||||
|
||||
forEach(properties, (value, key) => {
|
||||
const identify = new amplitude.Identify().set(key, value);
|
||||
amplitude.getInstance().identify(identify);
|
||||
});
|
||||
|
||||
window.ga('set', properties);
|
||||
const store = getStore();
|
||||
store.dispatch('analytics:updateUserProperties', properties);
|
||||
});
|
||||
}
|
||||
|
||||
export function setup () {
|
||||
// Setup queues until the real scripts are loaded
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// Amplitude
|
||||
amplitude.getInstance().init(AMPLITUDE_KEY);
|
||||
|
||||
// Google Analytics (aka Universal Analytics)
|
||||
window['GoogleAnalyticsObject'] = 'ga';
|
||||
window['ga'] = window['ga'] || function() {
|
||||
(window['ga'].q = window['ga'].q || []).push(arguments)
|
||||
}, window['ga'].l = 1 * new Date();
|
||||
ga('create', GA_ID);
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
export function load () {
|
||||
// Load real scripts
|
||||
if (!IS_PRODUCTION) return;
|
||||
|
||||
let firstScript = document.getElementsByTagName('script')[0];
|
||||
// Google Analytics
|
||||
const gaScript = document.createElement('script');
|
||||
[firstScript] = document.getElementsByTagName('script');
|
||||
gaScript.async = 1;
|
||||
gaScript.src = '//www.google-analytics.com/analytics.js';
|
||||
firstScript.parentNode.insertBefore(gaScript, firstScript);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ function loadLocale (i18nData) {
|
||||
script.type = 'text/javascript';
|
||||
script.text = i18nData.momentLang;
|
||||
head.appendChild(script);
|
||||
moment.locale(language.momentLangCode);
|
||||
moment.updateLocale(language.momentLangCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ import {
|
||||
} from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import {
|
||||
setup as setupAnalytics,
|
||||
} from '@/libs/analytics';
|
||||
import { setUpLogging } from '@/libs/logging';
|
||||
import router from './router/index';
|
||||
import getStore from './store';
|
||||
@@ -50,7 +47,6 @@ Vue.use(CollapsePlugin);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time
|
||||
const store = getStore();
|
||||
|
||||
if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
|
||||
|
||||
128
website/client/src/mixins/accountCreation.js
Normal file
128
website/client/src/mixins/accountCreation.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/es/lib/isEmail';
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
import hello from 'hellojs';
|
||||
import { buildAppleAuthUrl } from '../libs/auth';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
emailError: null,
|
||||
emailValid: false,
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
passwordValid: false,
|
||||
passwordInvalid: false,
|
||||
passwordConfirmValid: false,
|
||||
passwordConfirmInvalid: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
email () {
|
||||
this.validateEmail(this.email);
|
||||
},
|
||||
password () {
|
||||
this.validatePassword(this.password);
|
||||
},
|
||||
passwordConfirm () {
|
||||
this.validatePasswordConfirm(this.passwordConfirm);
|
||||
},
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
mounted () {
|
||||
hello.init({
|
||||
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
validateEmail: debounce(function valEmail (email) {
|
||||
if (!email) {
|
||||
this.emailValid = false;
|
||||
this.emailError = null;
|
||||
return;
|
||||
}
|
||||
if (!isEmail(email)) {
|
||||
this.emailValid = false;
|
||||
this.emailError = this.$t('enterValidEmail');
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:checkEmail', {
|
||||
email,
|
||||
}).then(res => {
|
||||
if (!res.valid) {
|
||||
this.emailValid = false;
|
||||
this.emailError = this.$t('cannotFulfillReq');
|
||||
return;
|
||||
}
|
||||
this.emailValid = true;
|
||||
this.emailError = null;
|
||||
});
|
||||
}, 500),
|
||||
validatePassword: debounce(function valPass (password) {
|
||||
if (!password) {
|
||||
this.passwordValid = false;
|
||||
this.passwordInvalid = false;
|
||||
return;
|
||||
}
|
||||
this.passwordValid = password.length >= MINIMUM_PASSWORD_LENGTH;
|
||||
this.passwordInvalid = password.length < MINIMUM_PASSWORD_LENGTH;
|
||||
}, 500),
|
||||
validatePasswordConfirm: debounce(function valPassConf (passwordConfirm) {
|
||||
if (!passwordConfirm) {
|
||||
this.passwordConfirmValid = false;
|
||||
this.passwordConfirmInvalid = false;
|
||||
return;
|
||||
}
|
||||
this.passwordConfirmValid = passwordConfirm === this.password;
|
||||
this.passwordConfirmInvalid = passwordConfirm !== this.password;
|
||||
}, 500),
|
||||
async proceed (accountType) {
|
||||
if (accountType === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
window.sessionStorage.removeItem('apple-token');
|
||||
}
|
||||
if (accountType === 'local') {
|
||||
this.$store.state.registrationOptions = {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
registrationMethod: 'local',
|
||||
};
|
||||
} else {
|
||||
this.authData = await this.socialAuth(accountType);
|
||||
const authId = await this.$store.dispatch('auth:socialAuth', {
|
||||
auth: this.authData,
|
||||
allowRegister: false,
|
||||
});
|
||||
if (authId) {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
this.$store.state.registrationOptions = {
|
||||
authData: this.authData,
|
||||
email: window.sessionStorage.getItem('social-email'),
|
||||
registrationMethod: accountType,
|
||||
};
|
||||
}
|
||||
}
|
||||
this.$router.push({ name: 'username', query: this.$route.query });
|
||||
},
|
||||
async socialAuth (network) {
|
||||
try {
|
||||
await hello(network).logout();
|
||||
} catch (e) {} // eslint-disable-line
|
||||
|
||||
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const auth = await hello(network).login({
|
||||
scope: 'email',
|
||||
// explicitly pass the redirect url or it might redirect to /home
|
||||
redirect_uri: redirectUrl, // eslint-disable-line camelcase
|
||||
});
|
||||
return auth;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -215,7 +215,7 @@ export default {
|
||||
eventCategory: 'behavior',
|
||||
demographics: appState.newGroup.demographics,
|
||||
type: appState.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
|
||||
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
uuid: user._id,
|
||||
taskType: task.type,
|
||||
direction,
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
if (!tasksScoredCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
|
||||
} else {
|
||||
|
||||
@@ -98,16 +98,10 @@
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 732px;
|
||||
flex: 0 0 751px;
|
||||
max-width: unset;
|
||||
|
||||
::v-deep {
|
||||
line-height: 1.71;
|
||||
|
||||
.small {
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -123,6 +117,14 @@
|
||||
width: 23%;
|
||||
}
|
||||
|
||||
small {
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.settings-label, .settings-value, a, p {
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.input-area .settings-label {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
>
|
||||
<button
|
||||
v-if="!hideSave"
|
||||
class="btn btn-save"
|
||||
class="btn btn-save mb-2"
|
||||
:class="primaryButtonColor ?? 'btn-primary'"
|
||||
type="submit"
|
||||
:disabled="disableSave"
|
||||
@@ -61,8 +61,4 @@ export default {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<table class="table">
|
||||
<user-id-row />
|
||||
<user-data-row />
|
||||
<privacy-preferences-row />
|
||||
<tr>
|
||||
<td colspan="3">
|
||||
</td>
|
||||
@@ -45,6 +46,7 @@
|
||||
<script>
|
||||
import UserIdRow from '@/pages/settings/siteDataRows/userIdRow.vue';
|
||||
import UserDataRow from '@/pages/settings/siteDataRows/userDataRow.vue';
|
||||
import PrivacyPreferencesRow from '@/pages/settings/siteDataRows/privacyPreferencesRow.vue';
|
||||
import ApiRow from '@/pages/settings/siteDataRows/apiRow.vue';
|
||||
import WebhooksRow from '@/pages/settings/siteDataRows/webhooksRow.vue';
|
||||
import DeveloperModeRow from '@/pages/settings/siteDataRows/developerModeRow.vue';
|
||||
@@ -56,6 +58,7 @@ export default {
|
||||
ApiRow,
|
||||
UserDataRow,
|
||||
UserIdRow,
|
||||
PrivacyPreferencesRow,
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
colspan="3"
|
||||
>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h3
|
||||
v-once
|
||||
class="gray-50 mb-0"
|
||||
>
|
||||
{{ $t('yourPrivacyPreferences') }}
|
||||
</h3>
|
||||
<a
|
||||
class="edit-link"
|
||||
@click.prevent="openModal()"
|
||||
>
|
||||
{{ $t('edit') }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
v-if="mixinData.inlineSettingMixin.modalVisible"
|
||||
colspan="3"
|
||||
>
|
||||
<h3
|
||||
v-once
|
||||
class="purple-200 mb-0"
|
||||
>
|
||||
{{ $t('yourPrivacyPreferences') }}
|
||||
</h3>
|
||||
<p
|
||||
v-once
|
||||
class="gray-50 mb-4"
|
||||
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"
|
||||
>
|
||||
<div class="w-66">
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center mb-1"
|
||||
>
|
||||
<label class="settings-label w-50 mb-0">
|
||||
{{ $t('performanceAnalytics') }}
|
||||
</label>
|
||||
<toggle-switch
|
||||
v-model="user.preferences.analyticsConsent"
|
||||
class="mb-auto"
|
||||
@change="prefToggled()"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-28p">
|
||||
<small class="gray-50">
|
||||
{{ $t('usedForSupport') }}
|
||||
</small>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex justify-content-between align-items-center mb-1"
|
||||
>
|
||||
<label class="settings-label w-50 mb-0">
|
||||
{{ $t('strictlyNecessary') }}
|
||||
</label>
|
||||
<toggle-switch
|
||||
:checked="true"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<small class="gray-50">
|
||||
{{ $t('requiredToRun') }}
|
||||
</small>
|
||||
<save-cancel-buttons
|
||||
class="mb-4"
|
||||
:disable-save="!mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues"
|
||||
@saveClicked="finalize()"
|
||||
@cancelClicked="requestCloseModal()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
small {
|
||||
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;
|
||||
}
|
||||
|
||||
.popover-box {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.w-66 {
|
||||
width: 66.7%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import SaveCancelButtons from '@/pages/settings/components/saveCancelButtons.vue';
|
||||
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: [
|
||||
GenericUserPreferencesMixin,
|
||||
InlineSettingMixin,
|
||||
],
|
||||
components: {
|
||||
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 () {
|
||||
const newVal = !this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues;
|
||||
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = newVal;
|
||||
},
|
||||
resetControls () {
|
||||
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
|
||||
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
|
||||
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -15,6 +15,7 @@
|
||||
<external-link-modal />
|
||||
<birthday-modal />
|
||||
<template v-if="isUserLoaded">
|
||||
<privacy-banner />
|
||||
<chat-banner />
|
||||
<damage-paused-banner />
|
||||
<gems-promo-banner />
|
||||
@@ -118,15 +119,15 @@ import { loadProgressBar } from 'axios-progress-bar';
|
||||
import birthdayModal from '@/components/news/birthdayModal';
|
||||
import AppMenu from '@/components/header/menu';
|
||||
import AppHeader from '@/components/header/index';
|
||||
import BirthdayBanner from '@/components/header/banners/birthdayBanner';
|
||||
import ChatBanner from '@/components/header/banners/chatBanner';
|
||||
import DamagePausedBanner from '@/components/header/banners/damagePaused';
|
||||
import GemsPromoBanner from '@/components/header/banners/gemsPromo';
|
||||
import GiftPromoBanner from '@/components/header/banners/giftPromo';
|
||||
import BirthdayBanner from '@/components/header/banners/birthdayBanner';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import AppFooter from '@/components/appFooter';
|
||||
import notificationsDisplay from '@/components/notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import BuyModal from '@/components/shops/buyModal.vue';
|
||||
import SelectMembersModal from '@/components/selectMembersModal.vue';
|
||||
import notifications from '@/mixins/notifications';
|
||||
@@ -159,6 +160,7 @@ export default {
|
||||
GemsPromoBanner,
|
||||
GiftPromoBanner,
|
||||
BirthdayBanner,
|
||||
PrivacyBanner,
|
||||
notificationsDisplay,
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
@@ -260,8 +262,13 @@ export default {
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
Analytics.setUser();
|
||||
Analytics.updateUser();
|
||||
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) {
|
||||
return null;
|
||||
|
||||
@@ -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
|
||||
@@ -76,12 +78,6 @@ const router = new VueRouter({
|
||||
// NOTE: when adding a new route entry make sure to implement the `common:setTitle` action
|
||||
// in the route component to set a specific subtitle for the page.
|
||||
routes: [
|
||||
{
|
||||
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'login', path: '/login', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{ name: 'logout', path: '/logout', component: Logout },
|
||||
{
|
||||
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
@@ -222,6 +218,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',
|
||||
@@ -302,7 +320,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
eventName: 'View Find Members',
|
||||
eventAction: 'View Find Members',
|
||||
eventCategory: 'behavior',
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
}
|
||||
|
||||
// Redirect old guild urls
|
||||
|
||||
@@ -6,13 +6,12 @@ const StaticWrapper = () => import('@/components/static/staticWrapper');
|
||||
const HomePage = () => import('@/components/static/home');
|
||||
|
||||
const AppleRedirectPage = () => import('@/components/static/appleRedirect');
|
||||
const ChatSunsetFaq = () => import('@/components/static/chatSunsetFaq');
|
||||
const ClearBrowserDataPage = () => import('@/components/static/clearBrowserData');
|
||||
const CommunityGuidelinesPage = () => import('@/components/static/communityGuidelines');
|
||||
const ContentScheduleFaq = () => import('@/components/static/contentScheduleFaq');
|
||||
const ContactPage = () => import('@/components/static/contact');
|
||||
const FAQPage = () => import('@/components/static/faq');
|
||||
const ChatSunsetFaq = () => import('@/components/static/chatSunsetFaq');
|
||||
const ContentScheduleFaq = () => import('@/components/static/contentScheduleFaq');
|
||||
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
|
||||
const FeaturesPage = () => import('@/components/static/features');
|
||||
const GroupPlansPage = () => import('@/components/static/groupPlans');
|
||||
// Commenting out merch page see
|
||||
@@ -22,6 +21,9 @@ const NewsPage = () => import('@/components/static/newStuff');
|
||||
const OverviewPage = () => import('@/components/static/overview');
|
||||
const PressKitPage = () => import('@/components/static/pressKit');
|
||||
const PrivacyPage = () => import('@/components/static/privacy');
|
||||
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
|
||||
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
|
||||
const TermsPage = () => import('@/components/static/terms');
|
||||
|
||||
export const STATIC_ROUTES = {
|
||||
@@ -55,6 +57,9 @@ export const STATIC_ROUTES = {
|
||||
{
|
||||
name: 'features', path: 'features', component: FeaturesPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'groupPlans', path: 'group-plans', component: GroupPlansPage, meta: { requiresLogin: false },
|
||||
},
|
||||
@@ -62,7 +67,7 @@ export const STATIC_ROUTES = {
|
||||
name: 'home', path: 'home', component: HomePage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
|
||||
name: 'login', path: '/login', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'news', path: 'new-stuff', component: NewsPage, meta: { requiresLogin: false },
|
||||
@@ -79,9 +84,21 @@ export const STATIC_ROUTES = {
|
||||
{
|
||||
name: 'privacy', path: 'privacy', component: PrivacyPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'privacyReview', path: 'privacy-review', component: PrivacyPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'terms', path: 'terms', component: TermsPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'termsReview', path: 'terms-review', component: TermsPage, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'username', path: '/username', component: RegisterUsername, meta: { requiresLogin: false },
|
||||
},
|
||||
{
|
||||
name: 'notFound', path: 'not-found', component: NotFoundPage, meta: { requiresLogin: false },
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -2,6 +2,10 @@ import axios from 'axios';
|
||||
|
||||
export async function trackEvent (store, params) {
|
||||
const url = `/analytics/track/${params.eventAction}`;
|
||||
|
||||
await axios.post(url, params);
|
||||
}
|
||||
|
||||
export async function updateUserProperties (store, params) {
|
||||
const url = '/analytics/update';
|
||||
await axios.post(url, params);
|
||||
}
|
||||
|
||||
@@ -64,34 +64,63 @@ export async function verifyDisplayName (store, params) {
|
||||
return result.data.data;
|
||||
}
|
||||
|
||||
export async function checkEmail (store, params) {
|
||||
const url = '/api/v4/user/auth/check-email';
|
||||
const result = await axios.post(url, {
|
||||
email: params.email,
|
||||
});
|
||||
|
||||
return result.data.data;
|
||||
}
|
||||
|
||||
export async function socialAuth (store, params) {
|
||||
const url = '/api/v4/user/auth/social';
|
||||
const result = await axios.post(url, {
|
||||
allowRegister: params.allowRegister,
|
||||
username: params.username,
|
||||
network: params.auth.network,
|
||||
authResponse: params.auth.authResponse,
|
||||
});
|
||||
|
||||
if (!result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
return user.id;
|
||||
}
|
||||
|
||||
export async function appleAuth (store, params) {
|
||||
const url = '/api/v4/user/auth/apple';
|
||||
const result = await axios.get(url, {
|
||||
params: {
|
||||
allowRegister: params.allowRegister,
|
||||
code: params.code,
|
||||
id_token: params.idToken,
|
||||
name: params.name,
|
||||
username: params.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (result.data.message && result.data.id_token) {
|
||||
return { idToken: result.data.id_token };
|
||||
}
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
saveLocalDataAuth(store, user.id, user.apiToken);
|
||||
return { id: user.id };
|
||||
}
|
||||
|
||||
export function logout (store, options = {}) {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';
|
||||
window.location.href = `/logout-server${query}`;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -120,7 +120,7 @@ export async function create (store, createdTask) {
|
||||
hitType: 'event',
|
||||
uuid,
|
||||
taskType: taskRes.type,
|
||||
}, { trackOnClient: true });
|
||||
});
|
||||
if (!tasksCreatedCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
|
||||
} else {
|
||||
|
||||
@@ -149,6 +149,7 @@ export default function clientStore () {
|
||||
question: false,
|
||||
},
|
||||
postLoadModal: '',
|
||||
registrationOptions: {},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import {
|
||||
describe, expect, test, beforeEach, afterEach,
|
||||
} from 'vitest';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
|
||||
import sinon from 'sinon';
|
||||
import Home from '@/components/static/home.vue';
|
||||
import Store from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Store);
|
||||
|
||||
describe('Home', () => {
|
||||
let registerStub;
|
||||
let socialAuthStub;
|
||||
let store;
|
||||
let wrapper;
|
||||
|
||||
function mountWrapper (query) {
|
||||
return shallowMount(Home, {
|
||||
store,
|
||||
localVue,
|
||||
mocks: {
|
||||
$t: string => string,
|
||||
$route: { query: query || {} },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function fillOutUserForm (username, email, password) {
|
||||
await wrapper.find('#usernameInput').setValue(username);
|
||||
await wrapper.find('input[type=email]').setValue(email);
|
||||
await wrapper.findAll('input[type=password]').setValue(password);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
registerStub = sinon.stub();
|
||||
socialAuthStub = sinon.stub();
|
||||
store = new Store({
|
||||
state: {},
|
||||
getters: {},
|
||||
actions: {
|
||||
'auth:register': registerStub,
|
||||
'auth:socialAuth': socialAuthStub,
|
||||
'auth:verifyUsername': () => async () => ({}),
|
||||
'common:setTitle': () => {},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(Analytics, 'track');
|
||||
|
||||
wrapper = mountWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('has a visible title', () => {
|
||||
expect(wrapper.find('h1').text()).to.equal('motivateYourself');
|
||||
});
|
||||
|
||||
describe('signup form', () => {
|
||||
test('registers a user from the form', async () => {
|
||||
const username = 'newUser';
|
||||
const email = 'rookie@habitica.com';
|
||||
const password = 'ImmaG3tProductive!';
|
||||
await fillOutUserForm(username, email, password);
|
||||
|
||||
await wrapper.find('form').trigger('submit');
|
||||
|
||||
expect(registerStub.calledOnce).to.be.true;
|
||||
expect(registerStub.getCall(0).args[1]).to.deep.equal({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirm: password,
|
||||
groupInvite: '',
|
||||
});
|
||||
});
|
||||
|
||||
test('registers a user with group invite if groupInvite in the query', async () => {
|
||||
const groupInvite = 'TheBestGroup';
|
||||
wrapper = mountWrapper({ groupInvite });
|
||||
await fillOutUserForm('invitedUser', 'invited@habitica.com', '1veGotFri3ndsHooray!');
|
||||
|
||||
await wrapper.find('form').trigger('submit');
|
||||
|
||||
expect(registerStub.calledOnce).to.be.true;
|
||||
expect(registerStub.getCall(0).args[1].groupInvite).to.equal(groupInvite);
|
||||
});
|
||||
|
||||
test('registers a user with group invite if p in the query', async () => {
|
||||
const p = 'ThePiGroup';
|
||||
wrapper = mountWrapper({ p });
|
||||
await fillOutUserForm('alsoInvitedUser', 'invited2@habitica.com', '1veGotFri3nds2!');
|
||||
|
||||
await wrapper.find('form').trigger('submit');
|
||||
|
||||
expect(registerStub.calledOnce).to.be.true;
|
||||
expect(registerStub.getCall(0).args[1].groupInvite).to.equal(p);
|
||||
});
|
||||
|
||||
test('registers a user with group invite invite if both p and groupInvite are in the query', async () => {
|
||||
const groupInvite = 'StillTheBestGroup';
|
||||
wrapper = mountWrapper({ p: 'LesserGroup', groupInvite });
|
||||
await fillOutUserForm('doublyInvitedUser', 'invited3@habitica.com', '1veGotSm4rtFri3nds!');
|
||||
|
||||
await wrapper.find('form').trigger('submit');
|
||||
|
||||
expect(registerStub.calledOnce).to.be.true;
|
||||
expect(registerStub.getCall(0).args[1].groupInvite).to.equal(groupInvite);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -26,17 +26,15 @@ const envVars = [
|
||||
'EMAILS_COMMUNITY_MANAGER_EMAIL',
|
||||
'EMAILS_TECH_ASSISTANCE_EMAIL',
|
||||
'EMAILS_PRESS_ENQUIRY_EMAIL',
|
||||
'GA_ID',
|
||||
'STRIPE_PUB_KEY',
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'APPLE_AUTH_CLIENT_ID',
|
||||
'AMPLITUDE_KEY',
|
||||
'LOGGLY_CLIENT_TOKEN',
|
||||
'TRUSTED_DOMAINS',
|
||||
'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 = {};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"FAQ": "ЧЗВ",
|
||||
"termsAndAgreement": "Натискайки бутона по-долу, Вие заявявате, че сте прочули и се съгласявате с <a href='/static/terms'>Условията за ползване</a> и <a href='/static/privacy'>Политиката за поверителност</a>.",
|
||||
"chores": "Домакинска работа",
|
||||
"clearBrowserData": "Изчистване на данните на браузъра",
|
||||
"communityExtensions": "Добавки и разширения",
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"justin": "Джъстин",
|
||||
"justinIntroMessage1": "Здравейте! Не съм Ви виждал досега. Името ми е <strong>Джъстин</strong> и аз ще Ви помогна да се ориентирате в Хабитика.",
|
||||
"justinIntroMessage3": "Чудесно! А сега, върху какво искате да работите по време на пътешествието си?",
|
||||
"justinIntroMessageUsername": "Преди да започнем, да решим как да Ви наричам. По-долу може да видите екранно име и потребителско име, които аз създадох за Вас. След като си изберете екранно име и потребителско име, ще преминем към създаването на герой!",
|
||||
"justinIntroMessageAppearance": "Е, как искате да изглеждате? Не се притеснявайте, можете да промените това по всяко време.",
|
||||
"introTour": "Готово! Аз попълних някои задачи с оглед на интересите Ви, така че можете да започнете веднага. Щракнете върху задача, за да я редактирате, или добавете нови задачи според желанията си!",
|
||||
"prev": "Назад",
|
||||
"next": "Напред",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"FAQ": "ČKD",
|
||||
"termsAndAgreement": "Kliknutím na tlačítko níže dáváš najevo, že jsi četl/a a souhlasíš s <a href='/static/terms'>Podmínkami užívání služby</a> a <a href='/static/privacy'>Ochranou soukromí</a>.",
|
||||
"chores": "Domácí práce",
|
||||
"clearBrowserData": "Vyčistit data v prohlížeči",
|
||||
"communityExtensions": "Doplňky & Rozšírení",
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"justin": "Justin",
|
||||
"justinIntroMessage1": "Ahoj, ty musíš být nový/á. Jmenuji se <strong>Justin</strong>, a budu tě provázet po světě Habitica.",
|
||||
"justinIntroMessage3": "Skvěle! Teď - na čem by jsi rád pracoval na tvé výpravě?",
|
||||
"justinIntroMessageUsername": "Než začneme, musíme vymyslet, jak Ti budeme říkat. Dole uvidíš veřejné a uživatelské jméno, které jsem pro tebe vygeneroval. Poté, co si vybereš své veřejné a uživatelské jméno, začneme s tvorbou avatara!",
|
||||
"justinIntroMessageAppearance": "Jak bys chtěl/a vypadat? Neboj se, můžeš svůj vzhled později změnit.",
|
||||
"introTour": "A jsme tu! Vyplnil jsem ti pár úkolů na základě tvých zájmů, takže můžeš ihned začít. Klikni na úkol pro jeho úpravu. nebo přidej nový úkol, který by odpovídal tvé rutině!",
|
||||
"prev": "Předch",
|
||||
"next": "Další",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"FAQ": "FAQ",
|
||||
"termsAndAgreement": "Ved at klikke på knappen for neden, indikerer du at du har læst og accepterer vores <a href='/static/terms'>Vilkår og betingelser</a> og <a href='/static/privacy'>Fortrolighedspolitik</a>.",
|
||||
"chores": "Pligter",
|
||||
"clearBrowserData": "Ryd browserdata",
|
||||
"communityExtensions": "Tilføjelser og udvidelser",
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
"justin": "Justin",
|
||||
"justinIntroMessage1": "Hejsa! Du må være ny her. Jeg hedder <strong>Justin</strong>, og jeg vil være din guide til Habitica.",
|
||||
"justinIntroMessage3": "Super! Nå, hvad er du så interesseret i at arbejde på gennem denne rejse?",
|
||||
"justinIntroMessageUsername": "Før vi begynder, så lad os finde ud af, hvad vi skal kalde dig. Forneden finder du et displaynavn og et brugernavn, jeg har lavet for dig. Efter du har valgt et displaynavn og brugernavn, kan vi lave din avatar!",
|
||||
"justinIntroMessageAppearance": "Hvordan har du lyst til at se ud? No worries, du kan altid ændre det senere.",
|
||||
"introTour": "Sådan! Jeg har lavet nogle Opgaver til dig baseret på dine interesser, så du kan komme i gang med det samme. Klik på en Opgave for at redigere den, eller tilføj nye Opgaver, som passer til din hverdag!",
|
||||
"prev": "Tidl.",
|
||||
"next": "Næste",
|
||||
|
||||
@@ -912,5 +912,14 @@
|
||||
"backgroundSirensLairNotes": "Wage es, in die Höhle einer Sirene zu tauchen.",
|
||||
"backgrounds082025": "SET 135: Veröffentlicht im August 2025",
|
||||
"backgroundSunnyStreetWithShopsText": "Sonnige Straße mit Läden",
|
||||
"backgroundSunnyStreetWithShopsNotes": "Genieße den Anblick und die Geräusche einer sonnigen Straße mit Läden."
|
||||
"backgroundSunnyStreetWithShopsNotes": "Genieße den Anblick und die Geräusche einer sonnigen Straße mit Läden.",
|
||||
"backgroundAutumnSwampText": "Herbstsumpf",
|
||||
"backgroundAutumnSwampNotes": "Nimm die eindringliche Atmosphäre eines Herbstsumpfes in dich auf.",
|
||||
"backgrounds102025": "SET 137: Veröffentlicht im Oktober 2025",
|
||||
"backgroundInsideForestWitchsCottageText": "Hütte der Waldhexe",
|
||||
"backgrounds092025": "SET 136: Veröffentlicht im September 2025",
|
||||
"backgroundInsideForestWitchsCottageNotes": "Webe Zauber in der Hütte der Waldhexe.",
|
||||
"backgroundCastleKeepWithBannersText": "Burghalle mit Bannern",
|
||||
"backgroundCastleKeepWithBannersNotes": "Singe Geschichten von Heldentaten in einer Burghalle mit Bannern.",
|
||||
"backgrounds112025": "SET 138: Veröffentlicht im November 2025"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"FAQ": "FAQ",
|
||||
"termsAndAgreement": "Durch Anklicken der nachfolgenden Schaltfläche erklärst Du Dich mit den <a href='/static/terms'>Nutzungsbedingungen</a> und <a href='/static/privacy'>Datenschutzbestimmungen</a>einverstanden.",
|
||||
"chores": "Hausarbeiten",
|
||||
"clearBrowserData": "Browserdaten löschen",
|
||||
"communityExtensions": "Add-ons & Erweiterungen",
|
||||
@@ -87,14 +86,14 @@
|
||||
"sync": "Synchronisieren",
|
||||
"tasks": "Aufgaben",
|
||||
"teams": "Teams",
|
||||
"terms": "AGB",
|
||||
"terms": "Nutzungsbedingungen",
|
||||
"tumblr": "Tumblr",
|
||||
"localStorageTryFirst": "Wenn Du Probleme mit Habitica hast, drücke den untenstehenden Knopf um lokale Browserdaten und die meisten Cookies zu löschen (andere Internetseiten sind davon nicht betroffen). Du wirst Dich danach neu einloggen müssen, damit das funktioniert, also halte Deine Anmeldedaten bereit. Du kannst sie unter Einstellungen -> <%= linkStart %>Seite<%= linkEnd %> einsehen.",
|
||||
"localStorageTryNext": "Wenn das Problem weiterhin besteht, <%= linkStart %>melde bitte einen Fehler<%= linkEnd %>, falls Du das noch nicht getan hast.",
|
||||
"localStorageClear": "Lokale Daten löschen",
|
||||
"localStorageClearExplanation": "Diese Schaltfläche löscht Deine lokalen Daten, sowie die meisten Cookies und loggt Dich aus.",
|
||||
"username": "Benutzername",
|
||||
"emailOrUsername": "E-Mail-Adresse oder Benutzername (Groß- und Kleinschreibung beachten)",
|
||||
"emailOrUsername": "Benutzername oder E-Mail-Adresse (Groß- und Kleinschreibung beachten)",
|
||||
"work": "Arbeit",
|
||||
"reportAccountProblems": "Melde Probleme mit Deinem Konto",
|
||||
"reportCommunityIssues": "Melde Community-Probleme",
|
||||
@@ -123,7 +122,7 @@
|
||||
"passwordConfirmationMatch": "Die Passwörter stimmen nicht überein.",
|
||||
"passwordResetPage": "Passwort zurücksetzen",
|
||||
"passwordReset": "Wenn wir Deine E-Mail-Adresse oder Deinen Benutzernamen kennen, wurden Anweisungen zum Passwort-Zurücksetzen dorthin verschickt.",
|
||||
"invalidLoginCredentialsLong": "Hoppla - Deine E-Mail-Adresse / Dein Benutzername oder Passwort ist nicht korrekt.\n- überprüfe die korrekte Schreibweise und beachte die Groß-/Kleinschreibung Deines Benutzernamens und Deiner E-Mail-Adresse.\n- Es ist möglich, dass Du Dich mit Facebook oder Google-sign-in statt mit Deiner E-Mail registriert hast. Probiere Dich damit anzumelden.\n- Wenn Du Dein Passwort vergessen hast, klicke auf \"Passwort vergessen\".",
|
||||
"invalidLoginCredentialsLong": "Deine E-Mail-Adresse, deine Benutzername oder Passwort sind nicht korrekt. Bitte versuche es erneut oder wähle \"Passwort vergessen.\"",
|
||||
"invalidCredentials": "Es gibt kein Konto, das diese Anmeldedaten verwendet.",
|
||||
"accountSuspended": "Dieser Account, Benutzer-ID \"<%= userId %>\", wurde gesperrt wegen Verletzung der Community-Richtlinien (https://habitica.com/static/community-guidelines) oder der Allgemeinen Geschäftsbedingungen (https://habitica.com/static/terms). Für genauere Angaben oder um die Aufgebung der Sperre zu erbitten, kontaktiere bitte unseren Community Manager unter <%= communityManagerEmail %> oder bitte Deine Eltern oder Erziehungsberechtigten ihm zu schreiben. Bitte nenne Deine @Benutzer-ID in der E-Mail.",
|
||||
"accountSuspendedTitle": "Dieser Account wurde suspendiert",
|
||||
@@ -133,12 +132,12 @@
|
||||
"invalidReqParams": "Ungültige Anfrageparameter.",
|
||||
"memberIdRequired": "\"member\" muss eine gültige UUID sein.",
|
||||
"heroIdRequired": "\"herold\" muss eine gültige UUID sein.",
|
||||
"cannotFulfillReq": "Deine Anfrage kann nicht erfüllt werden. Schreibe eine E-Mail an admin@habitica.com, falls dieser Fehler weiterhin auftritt.",
|
||||
"cannotFulfillReq": "Bitte gib eine gültige E-Mail-Adresse ein. Schreibe eine E-Mail an admin@habitica.com, falls dieser Fehler weiterhin auftritt.",
|
||||
"modelNotFound": "Diese Vorlage existiert nicht.",
|
||||
"signUpWithSocial": "Mit <%= social %> registrieren",
|
||||
"signUpWithSocial": "Mit <%= social %> fortfahren",
|
||||
"loginWithSocial": "Mit <%= social %> anmelden",
|
||||
"confirmPassword": "Passwort bestätigen",
|
||||
"usernameLimitations": "Der Benutzername muss zwischen 1 und 20 Buchstaben lang sein und darf nur die Buchstaben von A bis Z, Nummern von 0 bis 9, Bindestriche und Unterstriche beinhalten und darf keine unangemessenen Begriffe enthalten.",
|
||||
"usernameLimitations": "Benutzernamen können jederzeit geändert werden. Sie müssen zwischen 1 und 20 Zeichen lang sein und dürfen nur Buchstaben von A bis Z, Zahlen von 0 bis 9, Bindestriche und Unterstriche beinhalten.",
|
||||
"usernamePlaceholder": "z.B., HabitRabbit",
|
||||
"emailPlaceholder": "z.B., gryphon@beispiel.com",
|
||||
"passwordPlaceholder": "z.B., ******************",
|
||||
@@ -183,5 +182,10 @@
|
||||
"translateHabitica": "Habitica übersetzen",
|
||||
"marketing3Lead1Title": "Android & iOS Apps",
|
||||
"marketing4Lead3Button": "Starte noch heute",
|
||||
"emailBlockedRegistration": "Diese E-Mail ist für die Registrierung blockiert. Wenn du denkst, dass das ein Fehler ist, kontaktiere uns bitte unter admin@habitica.com."
|
||||
"emailBlockedRegistration": "Diese E-Mail ist für die Registrierung blockiert. Wenn du denkst, dass das ein Fehler ist, kontaktiere uns bitte unter admin@habitica.com.",
|
||||
"missingClientHeader": "Fehlende X-Client Header.",
|
||||
"minPasswordLengthLogin": "Dein Passwort ist mindestens 8 Zeichen lang.",
|
||||
"enterValidEmail": "Bitte gib eine gültige E-Mail-Adresse ein.",
|
||||
"whatToCallYou": "Wie sollen wir dich nennen?",
|
||||
"acceptPrivacyTOS": "Du bestätigst, dass du mindestens 18 Jahre alt bist und dass du unsere <a href='/static/terms' target='_blank'>Nutzungsbedingungen</a> und <a href='/static/privacy' target='_blank'>Datenschutz-Bestimmungen</a> gelesen hast und akzeptierst"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -3364,7 +3364,6 @@
|
||||
"headArmoireFlyFishingHatNotes": "Mit breiter Krempe und Platz für einen Ersatzköder. Du solltest nicht ohne diese Schönheit zum Fliegenfischen gehen. Erhöht Stärke und Wahrnehmung jeweils um <%= attrs %>. Verzauberter Schrank: Fliegenfischer-Set (Gegenstand 1 von 3)",
|
||||
"shieldArmoireFlyFishingRodNotes": "Hänge einen Köder an diese lange, flexible Rute und Fische werden ihn jedes Mal mit einem Insekt verwechseln. Erhöht Stärke und Intelligenz um jeweils <%= attrs %>. Verzauberter Schrank: Fliegenfischer-Set (Gegenstand 3 von 3)",
|
||||
"backMystery202506Text": "Sonnenschein-Aureole",
|
||||
"shieldSpecialSummer2025RogueText": "Tintenfisch-Tentakel",
|
||||
"shieldSpecialSummer2025RogueNotes": "Dieser Tentakel sieht biegsam aus, aber seine Saugnäpfe greifen fest zu. Feinde, passt auf! Erhöht die Stärke um <%= str %>. Limitierte Edition Sommer 2025 Ausrüstung.",
|
||||
"shieldSpecialSummer2025HealerText": "Ruderschnecken-Schild",
|
||||
"shieldSpecialSummer2025HealerNotes": "Dieser Schild sieht einfach aus, aber er hat gesundheitsschädliche Eigenschaften. Feinde, passt auf! Erhöht die Konstitution um <%= con %>. Limitierte Edition Sommer 2025 Ausrüstung.",
|
||||
@@ -3375,5 +3374,70 @@
|
||||
"backMystery202507Notes": "Dein Ross für Bürgersteige und Halfpipes. Gewährt keinen Attributbonus. Juli 2025 Abonnentengegenstand.",
|
||||
"shieldMystery202506Text": "Sonnenschein-Schild",
|
||||
"shieldMystery202506Notes": "Vertreibe die Dunkelheit und lasse deiner Umgebung warme und herzliche Strahlen zuteilwerden wo immer du auch bist. Gewährt keinen Attributbonus. Juni 2025 Abonnentengegenstand.",
|
||||
"backMystery202506Notes": "Trage ein warmes Leuchten mit dir, wenn du deinen täglichen Aufgaben nachgehst. Gewährt keinen Attributbonus. Juni 2025 Abonnentengegenstand."
|
||||
"backMystery202506Notes": "Trage ein warmes Leuchten mit dir, wenn du deinen täglichen Aufgaben nachgehst. Gewährt keinen Attributbonus. Juni 2025 Abonnentengegenstand.",
|
||||
"weaponSpecialFall2025WarriorText": "Bigfoot Axt",
|
||||
"weaponSpecialFall2025WarriorNotes": "Eine mächtige Waffe, um einen sicheren Pfad durch einen Herbstwald voller Komplikationen zu schlagen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Herbstausrüstung 2025.",
|
||||
"weaponSpecialFall2025RogueText": "Skelettklinge",
|
||||
"weaponSpecialFall2025RogueNotes": "Eine mächtige Waffe, um einen sicheren Pfad durch einen Herbstwald voller Hindernisse zu schlagen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Herbstausrüstung 2025.",
|
||||
"weaponSpecialFall2025HealerText": "Kobold Axt",
|
||||
"weaponSpecialFall2025HealerNotes": "Eine mächtige Waffe, um einen sicheren Pfad durch einen Herbstwald voller Erschwernisse zu schlagen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Herbstausrüstung 2025.",
|
||||
"weaponSpecialFall2025MageText": "Geist-mit-Maske Axt",
|
||||
"weaponSpecialFall2025MageNotes": "Eine mächtige Waffe, um einen sicheren Pfad durch einen Herbstwald voller Schrecken zu schlagen. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Herbstausrüstung 2025.",
|
||||
"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.",
|
||||
"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."
|
||||
}
|
||||
|
||||
@@ -278,5 +278,9 @@
|
||||
"summer2025ScallopWarriorSet": "Jakobsmuschel Krieger Set",
|
||||
"summer2025SquidRogueSet": "Tintenfisch Schurken Set",
|
||||
"summer2025SeaAngelHealerSet": "Ruderschnecken Heiler Set",
|
||||
"summer2025FairyWrasseMageSet": "Feenlippfisch Magier Set"
|
||||
"summer2025FairyWrasseMageSet": "Feenlippfisch Magier Set",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Krieger Set",
|
||||
"fall2025SkeletonRogueSet": "Skelett Schurken Set",
|
||||
"fall2025KoboldHealerSet": "Kobold Heiler Set",
|
||||
"fall2025MaskedGhostMageSet": "Maskengeist Magier Set"
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@
|
||||
"welcomeTo": "Willkommen in",
|
||||
"welcomeBack": "Willkommen zurück!",
|
||||
"justin": "Justin",
|
||||
"justinIntroMessage1": "Hallo! Du musst neu hier sein. Mein Name ist <strong>Justin</strong>, und ich werde Dein Reiseleiter in Habitica sein.",
|
||||
"justinIntroMessage1": "Hallo! Du musst neu hier sein. Ich bin <strong>Justin</strong>, ich werde dein Reiseleiter in Habitica sein. Also wie würdest du gerne aussehen? Keine Sorge, du kannst das später ändern.",
|
||||
"justinIntroMessage3": "Großartig! Woran möchtest Du auf dieser Reise arbeiten?",
|
||||
"justinIntroMessageUsername": "Bevor wir beginnen, sollten wir herausfinden, wie wir Dich nennen sollen. Unterhalb findest Du einen Anzeigenamen und einen Benutzernamen, den ich für Dich erzeugt habe. Nachdem Du einen Anzeigenamen und einen Benutzernamen gewählt hast, fangen wir mit Deinem Avatar an!",
|
||||
"justinIntroMessageAppearance": "Und wie möchtest Du aussehen? Keine Angst, Du kannst das auch später noch ändern.",
|
||||
"introTour": "Los geht's! Basierend auf Deinen Interessen habe ich Dir ein paar Aufgaben erstellt, damit Du gleich loslegen kannst. Klicke auf eine Aufgabe um sie zu bearbeiten oder erstelle neue Aufgaben, wie Du sie brauchst!",
|
||||
"prev": "Zurück",
|
||||
"next": "Vor",
|
||||
|
||||
@@ -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",
|
||||
@@ -257,5 +257,19 @@
|
||||
"changeClassDisclaimer": "Bei Änderung deiner Klasse bekommst du alle deine vorhandenen Attributspunkte erstattet. Passe deine Attributspunkte im Attributwerte-Abschnitt deines Profils an, sobald du eine neue Klasse gewählt hast.",
|
||||
"APITokenDisclaimer": "<b>Dein API-Schlüssel ist wie ein Passwort. Teile ihn niemals öffentlich.</b> Du wirst ggf. nach deiner Benutzer-ID gefragt, aber schreibe deinen API-Schlüssel niemals dort, wo andere ihn sehen könnten bspw. Github.<br><br><b>Hinweis:</b> Falls du ein neues API-Schlüssel brauchst (z.B., weil du ihn versehentlich geteilt hast), kannst du dein Passwort ändern, um ihn zurückzusetzen. Einmal zurückgesetzt, musst du dich auf all deinen Geräten, auf denen du Habitica verwendest, erneut anmelden und deinen neuen API-Schlüssel in Drittanbieter-Anwendungen angeben, die du verwendest.",
|
||||
"thirdPartyTools": "Drittanbieter Apps, Erweiterungen, und alle möglichen anderen Tools, die du mit deinem Account nutzen kannst, findest du im <a href='https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations' target='_blank'>Habitica Wiki</a>.",
|
||||
"transaction_subscription_bonus": "<b>Abonnement</b>-Bonus"
|
||||
"transaction_subscription_bonus": "<b>Abonnement</b>-Bonus",
|
||||
"acceptAllCookies": "Alle Cookies akzeptieren",
|
||||
"denyNonEssentialCookies": "Nicht-erforderliche Cookies ablehnen",
|
||||
"managePrivacyPreferences": "Privatsphäre-Einstellungen anpassen",
|
||||
"yourPrivacyPreferences": "Deine Privatsphäre-Einstellungen",
|
||||
"learnMorePrivacy": "Um mehr zu erfahren schau dir unsere <a href='/static/privacy' target='_blank'>Datenschutz-Bestimmungen</a> an.",
|
||||
"strictlyNecessary": "Technisch notwendig",
|
||||
"alwaysActive": "Immer aktiv",
|
||||
"requiredToRun": "Diese werden von unseren Apps und Websites benötigt um bestmöglich zu funktionieren.",
|
||||
"savePreferences": "Einstellungen speichern",
|
||||
"habiticaPrivacyPolicy": "Habiticas Datenschutz-Bestimmungen",
|
||||
"privacyOverview": "Heutzutage scheint jedes Unternehmen von deinen Daten profitieren zu wollen. Das kann es schwierig machen, die richtige App zur Verbesserung deiner Gewohnheiten zu finden. Habitica verwendet Cookies, die Daten nur zur Leistungsanalyse, zur Bearbeitung von Supportanfragen und zur Bereitstellung des bestmöglichen gamifizierten Erlebnisses speichern. Du kannst dies jederzeit in deinen Kontoeinstellungen ändern.",
|
||||
"privacySettingsOverview": "Habitica verwendet Cookies zur Leistungsanalyse, Bearbeitung von Supportanfragen und Bereitstellung des bestmöglichen gamifizierten Erlebnisses. Hierfür benötigen wir die folgenden Berechtigungen. Du kannst dies jederzeit in deinen Kontoeinstellungen ändern.",
|
||||
"performanceAnalytics": "Leistung und Analyse",
|
||||
"usedForSupport": "Diese werden verwendet, um Benutzererfahrung, Leistung und Dienste unserer Websites und Apps zu verbessern. Diese Daten werden von unserem Support-Team für die Bearbeitung von Anfragen und Fehlermeldungen verwendet."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user