Compare commits
99 Commits
v5.39.0
...
21fd83695a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21fd83695a | ||
|
|
2c44c37a96 | ||
|
|
f0adb85660 | ||
|
|
c80b94178b | ||
|
|
c85018a721 | ||
|
|
26a65d778b | ||
|
|
5bc4d7e99d | ||
|
|
20437c4188 | ||
|
|
e8c04e1109 | ||
|
|
e2d2a05315 | ||
|
|
be041f734d | ||
|
|
c430d2279c | ||
|
|
ef592cf35f | ||
|
|
f24cd10a79 | ||
|
|
2cd4e45016 | ||
|
|
8aaff7ae23 | ||
|
|
69a9fb89ef | ||
|
|
e8eeb76cab | ||
|
|
2029739a1b | ||
|
|
5cef106ea5 | ||
|
|
e096d7ac42 | ||
|
|
6db998e726 | ||
|
|
29c658b042 | ||
|
|
66710b8f38 | ||
|
|
c77db3d625 | ||
|
|
c947fa97d9 | ||
|
|
b2b9702797 | ||
|
|
e92503f032 | ||
|
|
65d5908898 | ||
|
|
8faa5b0582 | ||
|
|
95494c685b | ||
|
|
10978d46ab | ||
|
|
447eb6a0c4 | ||
|
|
3dec49b72c | ||
|
|
472d03f276 | ||
|
|
fd9a27c3ab | ||
|
|
a5c1423837 | ||
|
|
e9829b8b60 | ||
|
|
7ecb83dc7e | ||
|
|
e8ffe2286c | ||
|
|
fe63436a57 | ||
|
|
5b93b9b37a | ||
|
|
1d55027791 | ||
|
|
83f0984da1 | ||
|
|
53d4f75cab | ||
|
|
da45eb2adf | ||
|
|
3bf4af8d8b | ||
|
|
f030691fac | ||
|
|
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 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
@@ -72,7 +73,7 @@ export default async function processUsers () {
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
144
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.39.0",
|
||||
"version": "5.41.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.39.0",
|
||||
"version": "5.41.4",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -24,8 +24,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",
|
||||
@@ -56,10 +56,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",
|
||||
@@ -75,7 +75,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",
|
||||
@@ -6087,9 +6086,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -6906,30 +6906,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
|
||||
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.5",
|
||||
"bytes": "3.0.0",
|
||||
"compressible": "~2.0.16",
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"on-headers": "~1.0.2",
|
||||
"safe-buffer": "5.1.2",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -6943,6 +6936,35 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -7113,13 +7135,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-session": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz",
|
||||
"integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz",
|
||||
"integrity": "sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookies": "0.8.0",
|
||||
"cookies": "0.9.1",
|
||||
"debug": "3.2.7",
|
||||
"on-headers": "~1.0.2",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7164,9 +7187,10 @@
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"node_modules/cookies": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
|
||||
"integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
|
||||
"integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"keygrip": "~1.1.0"
|
||||
@@ -11425,9 +11449,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -14005,6 +14030,7 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz",
|
||||
"integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tsscmp": "1.0.6"
|
||||
},
|
||||
@@ -15519,15 +15545,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.0.2"
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@@ -16856,9 +16883,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@@ -20501,9 +20529,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
|
||||
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
@@ -21043,6 +21072,7 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
|
||||
"integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.x"
|
||||
}
|
||||
@@ -21368,26 +21398,6 @@
|
||||
"through2-filter": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-analytics": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz",
|
||||
"integrity": "sha512-HXSMyIcf2XTvwZ6ZZQLfxfViRm/yTGoRgDeTbojtq6rezeyKB0sTBcKH2fhddnteAHRcHiKgr/ACpbgjGOC6RQ==",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.1",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.18.2"
|
||||
}
|
||||
},
|
||||
"node_modules/universal-analytics/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||
|
||||
15
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.39.0",
|
||||
"version": "5.41.4",
|
||||
"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",
|
||||
@@ -51,10 +51,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 +70,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 +105,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'),
|
||||
|
||||
@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
|
||||
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
|
||||
});
|
||||
|
||||
it('highlights users with case-insensitive matching', async () => {
|
||||
const text = '@USER: message @User2 @USER3';
|
||||
const result = await highlightMentions(text);
|
||||
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
|
||||
});
|
||||
|
||||
it('doesn\'t highlight nonexisting users', async () => {
|
||||
const text = '@nouser message';
|
||||
const result = await highlightMentions(text);
|
||||
|
||||
@@ -12,11 +12,33 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let iapIsCanceledStub;
|
||||
let iapIsExpiredStub;
|
||||
let paymentBuySkuStub;
|
||||
let iapGetPurchaseDataStub;
|
||||
let validateGiftMessageStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup').resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
token = 'testToken';
|
||||
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
|
||||
receipt = `{"token": "${token}", "productId": "${sku}"}`;
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
|
||||
let paymentsCreateSubscritionStub; let
|
||||
iapGetPurchaseDataStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if no active subscription is found', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
|
||||
.returns(true);
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: -2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: 'subscription1month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
|
||||
});
|
||||
});
|
||||
|
||||
const subOptions = [
|
||||
{
|
||||
sku: 'subscription1month',
|
||||
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
let paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
token = 'test-token';
|
||||
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
paymentCancelSubscriptionSpy.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if we are missing a subscription', async () => {
|
||||
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapIsCanceledStub).to.be.calledOnce;
|
||||
expect(iapIsExpiredStub).to.be.calledOnce;
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
|
||||
@@ -11,12 +11,36 @@ const { i18n } = common;
|
||||
|
||||
describe('Google Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let iapSetupStub;
|
||||
let iapValidateStub;
|
||||
let iapIsValidatedStub;
|
||||
let paymentBuySkuStub;
|
||||
let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.isExpired.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
headers = {};
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
|
||||
describe('subscribe', () => {
|
||||
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
|
||||
nextPaymentProcessing;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
let paymentsCreateSubscritionStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sub = common.content.subscriptionBlocks[subKey];
|
||||
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
|
||||
describe('cancelSubscribe ', () => {
|
||||
let user; let token; let receipt; let signature; let headers; let customerId; let
|
||||
expirationDate;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
|
||||
let iapGetPurchaseDataStub; let
|
||||
paymentCancelSubscriptionSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
|
||||
signature = '';
|
||||
customerId = 'test-customerId';
|
||||
expirationDate = moment.utc();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({
|
||||
expirationDate,
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
|
||||
});
|
||||
|
||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||
iap.isCanceled.restore();
|
||||
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
|
||||
const laterDate = moment.utc().add(7, 'days');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
.returns([{
|
||||
startTimeMillis: expirationDate.valueOf(),
|
||||
expirationDate,
|
||||
autoRenewing: false,
|
||||
}, {
|
||||
startTimeMillis: laterDate.valueOf(),
|
||||
expirationDate: laterDate,
|
||||
autoRenewing: false,
|
||||
},
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ autoRenewing: true }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ expirationDate, autoRenewing: false }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
await expect(googlePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: googlePayments.constants.RESPONSE_STILL_VALID,
|
||||
});
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
|
||||
@@ -238,6 +238,18 @@ describe('POST /chat', () => {
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with case-insensitive mentions', async () => {
|
||||
const originalUsername = member.auth.local.username;
|
||||
const uppercaseUsername = originalUsername.toUpperCase();
|
||||
const messageWithMentions = `hi @${uppercaseUsername}`;
|
||||
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
|
||||
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||
|
||||
expect(newMessage.message.id).to.exist;
|
||||
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
|
||||
expect(groupMessages[0].id).to.exist;
|
||||
});
|
||||
|
||||
it('creates a chat with a max length of 3000 chars', async () => {
|
||||
const veryLongMessage = `
|
||||
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ describe('POST /user/auth/local/login', () => {
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
|
||||
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id, username: user.auth.local.username }),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
77
website/client/package-lock.json
generated
@@ -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",
|
||||
@@ -3393,9 +3393,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"
|
||||
@@ -4024,13 +4025,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"
|
||||
@@ -5081,12 +5084,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 +5122,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",
|
||||
@@ -5364,11 +5384,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 +7134,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",
|
||||
@@ -8515,9 +8551,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",
|
||||
|
||||
@@ -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
|
||||
@@ -208,17 +203,32 @@ export default {
|
||||
|
||||
return response;
|
||||
}, error => { // Set up Error interceptors
|
||||
if (!error.response) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (error.response.status >= 400) {
|
||||
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;
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 11 KiB |
9
website/client/src/assets/images/gifts_bg.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="378" height="176" viewBox="0 0 378 176" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0H378V174C378 175.105 377.105 176 376 176H1.99999C0.895423 176 0 175.105 0 174V0Z" fill="url(#paint0_linear_2257_239)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2257_239" x1="378" y1="0" x2="0" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#925CF3"/>
|
||||
<stop offset="1" stop-color="#34B5C1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
37
website/client/src/assets/images/gifts_start.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="48" height="96" viewBox="0 0 48 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.10104 12.0483C-2.82088 9.43721 -3.53422 6.57214 -5.6115 5.24584C-7.68877 3.91954 -9.89543 4.92709 -10.1422 6.808C-10.3891 8.68891 -9.06061 9.83066 -4.97737 13.9337C-3.81821 15.0985 -3.3812 14.6594 -3.10104 12.0483Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.34089 15.2054C4.45116 13.6561 7.27707 12.8443 9.45877 13.9889C11.6405 15.1334 11.8754 17.5575 10.3778 18.7127C8.88016 19.868 7.23193 19.2828 1.65411 17.781C0.0706697 17.3546 0.230624 16.7548 2.34089 15.2054Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.549002 12.0098C-3.61871 9.59194 -3.87667 15.8322 -2.20457 16.8023C-0.532473 17.7724 4.71671 14.4277 0.549002 12.0098Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L13.637 24.9825L9.18965 32.7229L-6.21656 23.785L-1.76917 16.0445Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.90457 13.0652L3.36623 19.0238L-1.08116 26.7643L-11.352 20.8057L-6.90457 13.0652Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L3.36623 19.0238L1.88377 21.604L-3.25163 18.6247L-1.76917 16.0445Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.21656 23.785L6.62195 31.2333L-3.75529 49.2944L-16.5938 41.8461L-6.21656 23.785Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.64886 25.2747L6.62195 31.2333L5.13948 33.8134L-5.13132 27.8548L-3.64886 25.2747Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.401307 24.1842L10.6721 30.1428L9.18965 32.7229L-1.08116 26.7643L0.401307 24.1842Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7924 38.4607L17.9387 42.0519L21.31 40.5834L24.8838 41.4413L23.4225 38.0537L24.2762 34.4625L20.9049 35.9309L17.3311 35.0731L18.7924 38.4607Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.93867 71.2331L-4.79238 74.8243L-1.42111 73.3559L2.15271 74.2137L0.691383 70.8261L1.54509 67.2349L-1.82618 68.7033L-5.4 67.8455L-3.93867 71.2331Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8949 25.3807L35.0583 29.8802L37.9424 26.2452L42.4202 25.0761L38.8028 22.178L37.6393 17.6786L34.7552 21.3135L30.2775 22.4826L33.8949 25.3807Z" fill="white" fill-opacity="0.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L40.579 68.1435L45.9507 88.2881L31.6312 92.1436L26.2596 71.999Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L26.2589 71.9966L31.6273 92.1421L17.3084 96L11.9401 75.8545Z" fill="#DDF3F3"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 23.3957 72.7701)" fill="#FFA624"/>
|
||||
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 26.2596 71.999)" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.9999 90.0369L30.8638 89.2658L31.6312 92.1436L28.7673 92.9147L27.9999 90.0369Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3957 72.7701L26.2596 71.999L27.0269 74.8768L24.163 75.6479L23.3957 72.7701Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L23.3951 72.7682L24.162 75.6461L12.707 78.7325L11.9401 75.8545Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5443 93.1213L27.9999 90.0369L28.7673 92.9147L17.3117 95.9991L16.5443 93.1213Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.1235 71.2279L40.579 68.1435L41.3464 71.0213L29.8908 74.1057L29.1235 71.2279Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.7277 88.4947L45.1833 85.4103L45.9507 88.2881L34.4951 91.3725L33.7277 88.4947Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.8638 89.2658L33.7277 88.4947L34.4951 91.3725L31.6312 92.1436L30.8638 89.2658Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L29.1235 71.2279L29.8908 74.1057L27.0269 74.8768L26.2596 71.999Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5224 56.3076C25.8087 53.7812 24.0792 51.3933 21.6588 50.9455C19.2383 50.4977 17.5679 52.2625 18.0403 54.0994C18.5126 55.9363 20.17 56.4948 25.4855 58.7621C26.9945 59.4057 27.236 58.834 26.5224 56.3076Z" stroke="#FFA624" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.745 57.1864C34.124 54.9555 36.4415 53.1391 38.8911 53.3791C41.3406 53.6191 42.4621 55.7782 41.5042 57.413C40.5463 59.0479 38.7999 59.1258 33.0684 59.8329C31.4413 60.0337 31.366 59.4173 32.745 57.1864Z" stroke="#FFBE5D" stroke-width="4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8923 54.898C25.1267 54.225 27.2139 60.108 29.1258 60.378C31.0378 60.648 34.6579 55.571 29.8923 54.898Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L46.8635 61.9994L45.6255 70.8503L28.0091 68.3625L29.247 59.5115Z" fill="#F8F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6306 57.0236L29.247 59.5114L28.0091 68.3624L10.3927 65.8745L11.6306 57.0236Z" fill="#DDF3F3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L35.1192 60.3408L33.8813 69.1917L22.137 67.5332L23.3749 58.6822Z" fill="#FFBE5D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.0091 68.3625L22.137 67.5332L23.3749 58.6822Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L35.1192 60.3408L34.7065 63.2911L28.8344 62.4618L29.247 59.5115Z" fill="#FFA624"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.8344 62.4618L22.9622 61.6326L23.3749 58.6822Z" fill="#EE9109"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8053 62.9241L22.5496 64.5827L22.137 67.533L10.3927 65.8745L10.8053 62.9241Z" fill="#C1E9E9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2939 66.2414L46.0382 67.9L45.6255 70.8503L33.8813 69.1917L34.2939 66.2414Z" fill="#DDF3F3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.0 KiB |
BIN
website/client/src/assets/images/home/signup-quill@2x.png
Normal file
|
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,14 +28,25 @@
|
||||
|
||||
&:disabled, &.disabled {
|
||||
cursor: default;
|
||||
color: $gray-50;
|
||||
opacity: 0.75;
|
||||
box-shadow: none;
|
||||
background-color: $gray-700;
|
||||
color: $gray-200;
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
border: 2px solid transparent;
|
||||
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba($black, 0.12),
|
||||
0 1px 2px 0 rgba($black, 0.24);
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
padding: 4px 12px;
|
||||
min-height: 32px;
|
||||
max-height: 32px;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
.svg {
|
||||
color: $gray-300;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +175,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 +252,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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
8
website/client/src/assets/svg/close-white.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g fill="#FFFFFF" fill-rule="nonzero">
|
||||
<polygon points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
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 |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.1792 31.6843L46.8536 22.3769L23.918 28.6988L18.861 42.5218L44.341 58.5813L58.1792 31.6843Z" fill="#FF944C"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L46.1108 26.1328L36.2812 28.8422L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M30.2393 39.0304L26.4518 31.5515L36.2813 28.8422L30.2393 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L36.2813 28.8422L30.2393 39.0304L46.6218 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.1108 26.1328L46.6218 34.5148L53.8301 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L26.4518 31.5516L30.2393 39.0304L23.0309 41.0173Z" fill="#FA8537"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.6218 34.5148L43.0424 53.79L53.8301 32.5279Z" fill="#FA8537"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L30.2393 39.0304L43.0425 53.79L23.0309 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L30.2393 39.0304L43.0425 53.79L46.6218 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.555 4.15937L47.026 0.420004L38.7773 1.59601L36.4144 6.17539L44.5675 12.8919L50.555 4.15937Z" fill="#FFBE5D"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L46.6034 1.6924L43.0682 2.1964L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M40.5221 5.46854L39.5331 2.7004L43.0682 2.1964L40.5221 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L43.0683 2.1964L40.5221 5.46855L46.414 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25894L46.6034 1.6924L46.414 4.62854L49.0064 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M37.9296 5.83815L39.5331 2.70041L40.5221 5.46855L37.9296 5.83815Z" fill="#FFA624"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25893L46.414 4.62853L44.3259 11.1688L49.0064 4.25893Z" fill="#FFA624"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M37.9297 5.83815L40.5221 5.46855L44.326 11.1688L37.9297 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L40.5221 5.46855L44.326 11.1688L46.414 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2986 16.7775L24.6513 8.36623L11.1016 3.94533L4.07056 9.19883L11.614 25.6769L27.2986 16.7775Z" fill="#FF6165"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L23.0573 10.0026L17.2502 8.10789L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M10.908 11.2141L11.4432 6.21322L17.2502 8.10789L10.908 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L17.2502 8.10789L10.9081 11.2141L20.5864 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L23.0573 10.0026L20.5864 14.3719L24.8449 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M6.64955 9.82464L11.4432 6.21321L10.908 11.2141L6.64955 9.82464Z" fill="#F23035"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L20.5864 14.3719L12.5221 22.8464L24.8449 15.7613Z" fill="#F23035"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.64959 9.82464L10.9081 11.2141L12.5221 22.8463L6.64959 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L10.9081 11.2141L12.5221 22.8463L20.5864 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.82083 31.6843L17.1464 22.3769L40.082 28.6988L45.139 42.5218L19.659 58.5813L5.82083 31.6843Z" fill="#24CC8F"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L17.8892 26.1328L27.7188 28.8422L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.7607 39.0304L37.5482 31.5515L27.7187 28.8422L33.7607 39.0304Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L27.7187 28.8422L33.7607 39.0304L17.3782 34.5148Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.8892 26.1328L17.3782 34.5148L10.1699 32.5279Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L37.5482 31.5516L33.7607 39.0304L40.9691 41.0173Z" fill="#1CA372"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.3782 34.5148L20.9576 53.79L10.1699 32.5279Z" fill="#1CA372"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L33.7607 39.0304L20.9575 53.79L40.9691 41.0173Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L33.7607 39.0304L20.9575 53.79L17.3782 34.5148Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.445 4.15937L16.974 0.420004L25.2227 1.59601L27.5856 6.17539L19.4325 12.8919L13.445 4.15937Z" fill="#925CF3"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L17.3966 1.6924L20.9318 2.1964L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M23.4779 5.46854L24.4669 2.7004L20.9318 2.1964L23.4779 5.46854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L20.9317 2.1964L23.4779 5.46855L17.586 4.62854Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25894L17.3966 1.6924L17.586 4.62854L14.9936 4.25894Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M26.0704 5.83815L24.4669 2.70041L23.4779 5.46855L26.0704 5.83815Z" fill="#4F2A93"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25893L17.586 4.62853L19.6741 11.1688L14.9936 4.25893Z" fill="#4F2A93"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M26.0703 5.83815L23.4779 5.46855L19.674 11.1688L26.0703 5.83815Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L23.4779 5.46855L19.674 11.1688L17.586 4.62854Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7014 16.7775L39.3487 8.36623L52.8984 3.94533L59.9294 9.19883L52.386 25.6769L36.7014 16.7775Z" fill="#50B5E9"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L40.9427 10.0026L46.7498 8.10789L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M53.092 11.2141L52.5568 6.21322L46.7498 8.10789L53.092 11.2141Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L46.7498 8.10789L53.0919 11.2141L43.4136 14.3719Z" fill="white"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L40.9427 10.0026L43.4136 14.3719L39.1551 15.7613Z" fill="white"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L52.5568 6.21321L53.092 11.2141L57.3504 9.82464Z" fill="#46A7D9"/>
|
||||
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L43.4136 14.3719L51.4779 22.8464L39.1551 15.7613Z" fill="#46A7D9"/>
|
||||
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L53.0919 11.2141L51.4779 22.8463L57.3504 9.82464Z" fill="white"/>
|
||||
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L53.0919 11.2141L51.4779 22.8463L43.4136 14.3719Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -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 |
@@ -117,7 +117,7 @@ export default {
|
||||
closeWithAction () {
|
||||
this.close();
|
||||
setTimeout(() => {
|
||||
this.$router.push({ name: 'achievements' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||
}, 200);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@ export default {
|
||||
watch: {
|
||||
userIdentifier () {
|
||||
this.isSearching = true;
|
||||
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||
this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
|
||||
this.isSearching = false;
|
||||
if (users.length === 1) {
|
||||
this.loadUser(users[0]._id);
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
class="row"
|
||||
>
|
||||
<div class="form col-12">
|
||||
<button
|
||||
class="btn btn-danger mt-3 float-right"
|
||||
@click="confirmDeleteHero"
|
||||
>
|
||||
Begin Member deletion
|
||||
</button>
|
||||
<basic-details
|
||||
:user-id="hero._id"
|
||||
:auth="hero.auth"
|
||||
@@ -96,6 +102,53 @@
|
||||
:reset-counter="resetCounter"
|
||||
@clear-data="clearData"
|
||||
/>
|
||||
<b-modal
|
||||
id="delete-member-modal"
|
||||
title="Delete Member"
|
||||
ok-title="Delete"
|
||||
ok-variant="danger"
|
||||
cancel-title="Cancel"
|
||||
@ok="deleteHero"
|
||||
>
|
||||
<b-modal-body>
|
||||
<p>
|
||||
Are you sure you want to delete this member?
|
||||
</p>
|
||||
<p class="errorMessage">
|
||||
Please note: This action cannot be undone!
|
||||
</p>
|
||||
<div class="ml-4">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAccountCheck"
|
||||
v-model="deleteHabiticaAccount"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAccountCheck"
|
||||
>
|
||||
Delete Habitica account
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="deleteAmplitudeCheck"
|
||||
v-model="deleteAmplitudeData"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="deleteAmplitudeCheck"
|
||||
>
|
||||
Delete Amplitude data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal-body>
|
||||
</b-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -184,6 +237,8 @@ export default {
|
||||
hasParty: false,
|
||||
partyNotExistError: false,
|
||||
adminHasPrivForParty: true,
|
||||
deleteHabiticaAccount: true,
|
||||
deleteAmplitudeData: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -249,6 +304,25 @@ export default {
|
||||
|
||||
this.resetCounter += 1; // tell child components to reinstantiate from scratch
|
||||
},
|
||||
confirmDeleteHero () {
|
||||
if (this.hero._id === this.user._id) {
|
||||
window.alert('You cannot delete your own account.');
|
||||
return;
|
||||
}
|
||||
this.$root.$emit('bv::show::modal', 'delete-member-modal');
|
||||
},
|
||||
deleteHero () {
|
||||
this.$store.dispatch('hall:deleteHero', {
|
||||
uuid: this.hero._id,
|
||||
deleteHabiticaAccount: this.deleteHabiticaAccount,
|
||||
deleteAmplitudeData: this.deleteAmplitudeData,
|
||||
}).then(() => {
|
||||
this.$root.$emit('bv::hide::modal', 'delete-member-modal');
|
||||
this.$router.push({ name: 'adminPanel' });
|
||||
}).catch(err => {
|
||||
window.alert(err);
|
||||
});
|
||||
},
|
||||
hasUnsavedChanges (...comparisons) {
|
||||
for (const index in comparisons) {
|
||||
if (index && comparisons[index]) {
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
Party ID
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
{{ groupPartyData._id }}
|
||||
<router-link
|
||||
:to="{'name': 'groupAdminGroup', 'params': {'groupId': groupPartyData._id}}"
|
||||
>
|
||||
{{ groupPartyData._id }}
|
||||
</router-link>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
|
||||
@@ -15,6 +15,25 @@
|
||||
:class="{ 'open': expand }"
|
||||
>
|
||||
Subscription, Monthly Perks
|
||||
<span
|
||||
v-if="isSubscribed() && !isCancelled()"
|
||||
class="text-success float-right ml-3"
|
||||
>
|
||||
Active
|
||||
</span>
|
||||
<span
|
||||
v-else-if="isSubscribed() && isCancelled()"
|
||||
class="text-success float-right ml-3"
|
||||
>
|
||||
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
|
||||
</span>
|
||||
<span
|
||||
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
|
||||
class="text-warning float-right ml-3"
|
||||
>
|
||||
Inactive
|
||||
</span>
|
||||
|
||||
<b
|
||||
v-if="hasUnsavedChanges && !expand"
|
||||
class="text-warning float-right"
|
||||
@@ -46,7 +65,7 @@
|
||||
class="form-control"
|
||||
type="text"
|
||||
>
|
||||
<option value="groupPlan">
|
||||
<option value="Group Plan">
|
||||
Group Plan
|
||||
</option>
|
||||
<option value="Stripe">
|
||||
@@ -154,7 +173,11 @@
|
||||
>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">
|
||||
{{ group.name }}
|
||||
<router-link
|
||||
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
|
||||
>
|
||||
{{ group.name }}
|
||||
</router-link>
|
||||
<small class="float-right">{{ group._id }}</small>
|
||||
</h6>
|
||||
<p class="card-text">
|
||||
@@ -245,8 +268,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<small
|
||||
v-if="!hero.purchased.plan.dateTerminated
|
||||
&& hero.purchased.plan.planId"
|
||||
v-if="isSubscribed() && !isCancelled()"
|
||||
class="text-success"
|
||||
>
|
||||
The subscription does not have a termination date and is active.
|
||||
@@ -419,6 +441,79 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<h2>Payment Details</h2>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm"
|
||||
@click="getSubscriptionPaymentDetails"
|
||||
>
|
||||
Get Subscription Payment Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="paymentDetails"
|
||||
>
|
||||
<div
|
||||
v-for="(value, key) in paymentDetails"
|
||||
:key="key"
|
||||
class="form-group row"
|
||||
>
|
||||
<label class="col-sm-3 col-form-label">
|
||||
{{ getHumanReadablePaymentDetails(key).label }}:
|
||||
<span
|
||||
:id="`${key}_tooltip`"
|
||||
v-b-tooltip.hover.right="getHumanReadablePaymentDetails(key).help"
|
||||
class="info-icon"
|
||||
>?</span>
|
||||
</label>
|
||||
<strong class="col-sm-9 col-form-label">
|
||||
<span v-if="value === true">Yes</span>
|
||||
<span v-else-if="value === false">No</span>
|
||||
<span
|
||||
v-else-if="value instanceof String && isDate(value)"
|
||||
v-b-tooltip.hover="value"
|
||||
>
|
||||
{{ formatDate(value) }}
|
||||
</span>
|
||||
<span v-else-if="value === null">---</span>
|
||||
<span v-else>{{ value }}</span>
|
||||
</strong>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="offset-sm-3 col-sm-9">
|
||||
<a
|
||||
v-if="hero.purchased.plan.paymentMethod === 'Google'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="playOrdersUrl"
|
||||
>
|
||||
Play Console
|
||||
</a>
|
||||
<a
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
|
||||
>
|
||||
PayPal Dashboard
|
||||
</a>
|
||||
<a
|
||||
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
|
||||
class="btn btn-primary btn-sm"
|
||||
target="_blank"
|
||||
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
|
||||
>
|
||||
Stripe Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="expand"
|
||||
@@ -474,17 +569,36 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
.form-group {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
width: auto;
|
||||
|
||||
.input-group-text {
|
||||
border-bottom-right-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
font-size: 0.8rem;
|
||||
color: $purple-400;
|
||||
cursor: pointer;
|
||||
margin-left: 0.2rem;
|
||||
background-color: $gray-500;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
|
||||
.info-icon:hover {
|
||||
background-color: $purple-400;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -495,6 +609,55 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
label: 'Customer ID',
|
||||
help: 'The unique identifier for the customer in the payment system.',
|
||||
},
|
||||
purchaseDate: {
|
||||
label: 'Purchase Date',
|
||||
help: 'The date when the subscription was purchased or renewed.',
|
||||
},
|
||||
originalPurchaseDate: {
|
||||
label: 'Original Purchase Date',
|
||||
help: 'The date when the subscription was first purchased.',
|
||||
},
|
||||
productId: {
|
||||
label: 'Product ID',
|
||||
help: 'The identifier for the product associated with the subscription.',
|
||||
},
|
||||
transactionId: {
|
||||
label: 'Transaction ID',
|
||||
help: 'The unique identifier for the last transaction in the payment system.',
|
||||
},
|
||||
isCanceled: {
|
||||
label: 'Is Canceled',
|
||||
help: 'Indicates whether the subscription has been canceled by the user or the system.',
|
||||
},
|
||||
isExpired: {
|
||||
label: 'Is Expired',
|
||||
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
|
||||
},
|
||||
expirationDate: {
|
||||
label: 'Termination Date',
|
||||
help: 'The date when the subscription will expire or has expired.',
|
||||
},
|
||||
nextPaymentDate: {
|
||||
label: 'Next Payment Date',
|
||||
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
|
||||
},
|
||||
lastPaymentDate: {
|
||||
label: 'Last Payment Date',
|
||||
help: 'The date when the lastpayment was made for the subscription.',
|
||||
},
|
||||
failedPayments: {
|
||||
label: 'Failed Payments',
|
||||
help: 'Number of times the payment failed for this subscription.',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpinner,
|
||||
@@ -520,6 +683,7 @@ export default {
|
||||
isConvertingToGroupPlan: false,
|
||||
groupPlanID: '',
|
||||
subscriptionBlocks,
|
||||
paymentDetails: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -553,6 +717,9 @@ export default {
|
||||
}
|
||||
return terminationDate;
|
||||
},
|
||||
playOrdersUrl () {
|
||||
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dateFormat (date) {
|
||||
@@ -583,6 +750,20 @@ export default {
|
||||
this.isConvertingToGroupPlan = true;
|
||||
this.hero.purchased.plan.owner = '';
|
||||
},
|
||||
getSubscriptionPaymentDetails () {
|
||||
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
|
||||
.then(details => {
|
||||
if (details) {
|
||||
this.paymentDetails = details;
|
||||
} else {
|
||||
alert('No payment details found.');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching subscription payment details:', error);
|
||||
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
|
||||
});
|
||||
},
|
||||
saveClicked (e) {
|
||||
e.preventDefault();
|
||||
if (this.isConvertingToGroupPlan) {
|
||||
@@ -601,6 +782,31 @@ export default {
|
||||
this.$emit('changeUserIdentifier', id);
|
||||
}
|
||||
},
|
||||
getHumanReadablePaymentDetails (key) {
|
||||
return humanReadablePaymentDetails[key] || { label: key, help: '' };
|
||||
},
|
||||
isDate (date) {
|
||||
return moment(date).isValid();
|
||||
},
|
||||
formatDate (date) {
|
||||
return date ? moment(date).format('MM/DD/YYYY') : '---';
|
||||
},
|
||||
isSubscribed () {
|
||||
console.log(this.hero.purchased.plan.customerId, this.hero.purchased.plan.dateTerminated);
|
||||
return this.hero.purchased.plan
|
||||
&& this.hero.purchased.plan.customerId
|
||||
&& this.hero.purchased.plan.planId
|
||||
&& this.hero.purchased.plan.paymentMethod
|
||||
&& (
|
||||
!this.hero.purchased.plan.dateTerminated
|
||||
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
|
||||
);
|
||||
},
|
||||
isCancelled () {
|
||||
return this.hero.purchased.plan
|
||||
&& this.hero.purchased.plan.dateTerminated
|
||||
&& this.hero.purchased.plan.dateTerminated !== '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -226,7 +226,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async retrieveUserHistory () {
|
||||
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
|
||||
const history = await this.$store.dispatch('admin:getUserHistory', { userIdentifier: this.hero._id });
|
||||
this.armoire = history.armoire;
|
||||
this.questInviteResponses = history.questInviteResponses;
|
||||
this.cron = history.cron;
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
>
|
||||
{{ $t('adminPanel') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'groupSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'groupAdmin'}"
|
||||
>
|
||||
{{ $t('groupAdmin') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'accessControl')"
|
||||
class="nav-link"
|
||||
|
||||
47
website/client/src/components/admin/formRow.vue
Normal file
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -43,9 +43,11 @@ export default {
|
||||
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
|
||||
const parseSettings = JSON.parse(AUTH_SETTINGS);
|
||||
const userId = parseSettings ? parseSettings.auth.apiId : '';
|
||||
const username = this.$store?.state?.user?.data?.auth?.local?.username || '';
|
||||
|
||||
return this.$t('accountSuspended', {
|
||||
userId,
|
||||
username,
|
||||
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
@update-challenge="updateChallenge"
|
||||
/>
|
||||
<close-challenge-modal
|
||||
:members="members"
|
||||
:challenge-id="challenge._id"
|
||||
:prize="challenge.prize"
|
||||
:flag-count="challenge.flagCount"
|
||||
@@ -72,32 +71,40 @@
|
||||
</div>
|
||||
<div class="col-12 col-md-6 text-right">
|
||||
<div
|
||||
class="box member-count"
|
||||
class="box member-count p-2"
|
||||
@click="showMemberModal()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon member-icon"
|
||||
v-html="icons.memberIcon"
|
||||
></div>
|
||||
{{ challenge.memberCount }}
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('participantsTitle') }}
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
class="svg-icon member-icon"
|
||||
v-html="icons.memberIcon"
|
||||
></div>
|
||||
<span class="number">{{ challenge.memberCount }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('participantsTitle') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div
|
||||
class="svg-icon gem-icon"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
{{ challenge.prize || 0 }}
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('prize') }}
|
||||
<div class="box prize-count p-2">
|
||||
<div class="box-content">
|
||||
<div class="icon-number-row">
|
||||
<div
|
||||
class="svg-icon gem-icon"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
<span class="number">{{ challenge.prize || 0 }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="details"
|
||||
>
|
||||
{{ $t('prize') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,7 +311,6 @@
|
||||
|
||||
.box {
|
||||
display: inline-block;
|
||||
padding: 1em;
|
||||
border-radius: 2px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
@@ -314,22 +320,88 @@
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
vertical-align: bottom;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.member-count:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-number-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 0.1em;
|
||||
|
||||
.number {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
width: 30px;
|
||||
display: inline-block;
|
||||
margin-right: .2em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 12px;
|
||||
margin-top: 0.4em;
|
||||
color: $gray-200;
|
||||
width: 100%;
|
||||
padding: 0 4px;
|
||||
line-height: 1.15;
|
||||
word-break: break-word;
|
||||
max-height: 2.3em;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&.member-count {
|
||||
.icon-number-row {
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
max-height: 2.2em;
|
||||
}
|
||||
}
|
||||
|
||||
&.prize-count {
|
||||
.icon-number-row {
|
||||
.svg-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: 11px;
|
||||
line-height: 1.1;
|
||||
max-height: 2.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,7 +696,6 @@ export default {
|
||||
this.members = [];
|
||||
},
|
||||
closeChallenge () {
|
||||
this.initialMembersLoad();
|
||||
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
|
||||
},
|
||||
edit () {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
id="close-challenge-modal"
|
||||
:title="$t('endChallenge')"
|
||||
size="md"
|
||||
:hide-header="false"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
@@ -15,6 +16,9 @@
|
||||
>
|
||||
{{ $t('endChallenge') }}
|
||||
</h2>
|
||||
<close-x
|
||||
@close="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
|
||||
/>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<span
|
||||
@@ -28,28 +32,67 @@
|
||||
class="col-12"
|
||||
>
|
||||
<div class="col-12">
|
||||
<div class="support-habitica">
|
||||
<!-- @TODO: Add challenge achievement badge here-->
|
||||
<div class="badge-section">
|
||||
<div
|
||||
class="gems-left"
|
||||
v-html="icons.gemsOrange"
|
||||
></div>
|
||||
<div
|
||||
class="challenge-badge"
|
||||
v-html="icons.endChallengeBadge"
|
||||
></div>
|
||||
<div
|
||||
class="gems-right"
|
||||
v-html="icons.gemsPurple"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<member-search-dropdown
|
||||
:text="winnerText"
|
||||
:members="members"
|
||||
:challenge-id="challengeId"
|
||||
@member-selected="selectMember"
|
||||
/>
|
||||
<div class="col-12 search-input-container">
|
||||
<div class="search-input-wrapper">
|
||||
<div
|
||||
class="search-icon"
|
||||
v-html="icons.search"
|
||||
></div>
|
||||
<input
|
||||
v-model="searchTerm"
|
||||
class="search-input"
|
||||
type="text"
|
||||
placeholder="@Username"
|
||||
@input="searchMembers"
|
||||
@focus="showResults = true"
|
||||
@blur="handleBlur"
|
||||
>
|
||||
<div
|
||||
v-if="showResults && filteredMembers.length > 0"
|
||||
class="search-results"
|
||||
>
|
||||
<div
|
||||
v-for="member in filteredMembers"
|
||||
:key="member._id"
|
||||
class="search-result-item"
|
||||
@mousedown="selectMember(member)"
|
||||
>
|
||||
{{ getMemberDisplayName(member) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
class="btn award-winner-btn"
|
||||
:class="{'has-winner': winner._id}"
|
||||
:disabled="!winner._id"
|
||||
@click="closeChallenge"
|
||||
>
|
||||
{{ $t('awardWinners') }}
|
||||
<span>{{ $t('awardWinners') }}</span>
|
||||
<div
|
||||
class="gem-icon"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
@@ -60,14 +103,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
||||
<strong
|
||||
v-once
|
||||
class="delete-challenge-text"
|
||||
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="col-12 refund-text"
|
||||
>
|
||||
{{ $t('deleteChallengeRefundDescription') }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-danger"
|
||||
class="btn btn-danger delete-challenge-btn"
|
||||
@click="deleteChallenge()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon color delete-icon"
|
||||
v-html="icons.deleteIcon"
|
||||
></div>
|
||||
{{ $t('deleteChallenge') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -82,6 +138,7 @@
|
||||
|
||||
<style lang='scss'>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
@import '@/assets/scss/button.scss';
|
||||
|
||||
#close-challenge-modal {
|
||||
h2 {
|
||||
@@ -94,26 +151,190 @@
|
||||
|
||||
.header-wrap {
|
||||
width: 100%;
|
||||
padding-top: 2em;
|
||||
padding-top: 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.support-habitica {
|
||||
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
|
||||
width: 325px;
|
||||
height: 89px;
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
margin-top: 1em !important;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-55%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: $gray-200;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding-left: 36px;
|
||||
padding-right: 12px;
|
||||
border: 1px solid $gray-400;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, border-width 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: $white;
|
||||
border: 1px solid $gray-400;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.search-result-item {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: $purple-600;
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-challenge-text {
|
||||
color: $maroon-50;
|
||||
}
|
||||
|
||||
.refund-text {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
color: $gray-50;
|
||||
margin-top: 0.5em !important;
|
||||
}
|
||||
|
||||
.delete-challenge-btn {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.delete-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.award-winner-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 4px 12px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:not(:disabled) {
|
||||
background-color: $white;
|
||||
color: $gray-200;
|
||||
border: 1px solid $gray-400;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
|
||||
&.has-winner {
|
||||
background-color: $purple-200;
|
||||
color: $white;
|
||||
border-color: $purple-200;
|
||||
}
|
||||
|
||||
&:hover:not(.has-winner) {
|
||||
background-color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: $gems-color;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
margin: -24px auto 0;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
.gems-left, .gems-right {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.challenge-badge {
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer, .modal-header {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.footer-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-12 {
|
||||
margin-top: 2em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
.col-12:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.or {
|
||||
@@ -123,21 +344,41 @@
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
font-weight: bold;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
|
||||
import debounce from 'lodash/debounce';
|
||||
import searchIcon from '@/assets/svg/for-css/search.svg?raw';
|
||||
import deleteIcon from '@/assets/svg/delete.svg?raw';
|
||||
import gemIcon from '@/assets/svg/gem.svg?raw';
|
||||
import endChallengeBadge from '@/assets/svg/for-css/end_challenge_badge.svg?raw';
|
||||
import gemsOrange from '@/assets/svg/for-css/orange100_red100_yellow100_gems.svg?raw';
|
||||
import gemsPurple from '@/assets/svg/for-css/purple200_green10_blue100_gems.svg?raw';
|
||||
import closeX from '@/components/ui/closeX';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
closeX,
|
||||
},
|
||||
props: ['challengeId', 'members', 'prize', 'flagCount'],
|
||||
props: ['challengeId', 'prize', 'flagCount'],
|
||||
data () {
|
||||
return {
|
||||
winner: {},
|
||||
searchTerm: '',
|
||||
showResults: false,
|
||||
filteredMembers: [],
|
||||
isSearching: false,
|
||||
icons: Object.freeze({
|
||||
search: searchIcon,
|
||||
deleteIcon,
|
||||
gem: gemIcon,
|
||||
endChallengeBadge,
|
||||
gemsOrange,
|
||||
gemsPurple,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -149,9 +390,58 @@ export default {
|
||||
return this.flagCount > 0;
|
||||
},
|
||||
},
|
||||
created () {
|
||||
this.searchMembersDebounced = debounce(this.performSearch, 500);
|
||||
},
|
||||
methods: {
|
||||
searchMembers () {
|
||||
if (!this.searchTerm) {
|
||||
this.filteredMembers = [];
|
||||
this.isSearching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSearching = true;
|
||||
this.searchMembersDebounced();
|
||||
},
|
||||
async performSearch () {
|
||||
if (!this.searchTerm) {
|
||||
this.filteredMembers = [];
|
||||
this.isSearching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const searchTerm = this.searchTerm.replace('@', '');
|
||||
|
||||
try {
|
||||
const members = await this.$store.dispatch('members:getChallengeMembers', {
|
||||
challengeId: this.challengeId,
|
||||
searchTerm,
|
||||
includeAllPublicFields: true,
|
||||
});
|
||||
|
||||
this.filteredMembers = members.slice(0, 10);
|
||||
} catch (err) {
|
||||
this.filteredMembers = [];
|
||||
} finally {
|
||||
this.isSearching = false;
|
||||
}
|
||||
},
|
||||
getMemberDisplayName (member) {
|
||||
if (member.auth?.local?.username) {
|
||||
return `@${member.auth.local.username}`;
|
||||
}
|
||||
return member.profile?.name || '';
|
||||
},
|
||||
selectMember (member) {
|
||||
this.winner = member;
|
||||
this.searchTerm = this.getMemberDisplayName(member);
|
||||
this.showResults = false;
|
||||
},
|
||||
handleBlur () {
|
||||
setTimeout(() => {
|
||||
this.showResults = false;
|
||||
}, 200);
|
||||
},
|
||||
async closeChallenge () {
|
||||
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -82,9 +82,7 @@
|
||||
<select-translated-array
|
||||
:items="[
|
||||
'groupParentChildren',
|
||||
'groupCouple',
|
||||
'groupFriends',
|
||||
'groupCoworkers',
|
||||
'groupManager',
|
||||
'groupTeacher'
|
||||
]"
|
||||
|
||||
@@ -218,13 +218,19 @@
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
|
||||
// somehow the browser felt like setting this 398px instead
|
||||
// now its fixed to 400 :)
|
||||
width: 400px;
|
||||
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
@media (max-width: 589px) {
|
||||
max-width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.quest-col {
|
||||
::v-deep {
|
||||
.item-wrapper {
|
||||
@@ -251,6 +257,28 @@
|
||||
::v-deep & {
|
||||
.modal-dialog {
|
||||
width: 448px !important;
|
||||
max-width: calc(100vw - 20px);
|
||||
margin: 0.5rem auto;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
width: 100% !important;
|
||||
margin: 0.25rem auto;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
126
website/client/src/components/header/banners/privacy.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div
|
||||
class="banner d-flex align-items-center justify-content-between py-3 px-4"
|
||||
id="privacy-banner"
|
||||
v-if="!hidden"
|
||||
>
|
||||
<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>
|
||||
@@ -1,37 +1,43 @@
|
||||
<template>
|
||||
<div
|
||||
class="notification d-flex flex-column justify-content-center text-center"
|
||||
class="notification d-flex justify-content-center align-items-center"
|
||||
>
|
||||
<strong
|
||||
v-once
|
||||
class="mx-auto mb-2"
|
||||
<img
|
||||
src="@/assets/images/gifts_start.svg"
|
||||
class="gift-start"
|
||||
alt=""
|
||||
>
|
||||
{{ $t('g1g1') }}
|
||||
</strong>
|
||||
<small
|
||||
v-once
|
||||
class="mx-4 mb-3"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</small>
|
||||
<div
|
||||
class="btn-secondary mx-auto d-flex"
|
||||
@click="showSelectUser()"
|
||||
>
|
||||
<div
|
||||
<div class="content-wrapper d-flex flex-column justify-content-center text-center">
|
||||
<strong
|
||||
v-once
|
||||
class="m-auto"
|
||||
class="mx-auto mb-2"
|
||||
>
|
||||
{{ $t('g1g1') }}
|
||||
</strong>
|
||||
<small
|
||||
v-once
|
||||
class="mx-4 mb-3"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</small>
|
||||
<button
|
||||
class="btn btn-secondary mx-auto"
|
||||
@click="showSelectUser()"
|
||||
>
|
||||
{{ $t('sendGift') }}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src="@/assets/images/gifts_start.svg"
|
||||
class="gift-end"
|
||||
alt=""
|
||||
>
|
||||
<div
|
||||
class="notification-remove"
|
||||
@click.stop="remove()"
|
||||
class="close-x"
|
||||
@click="remove()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
class="svg-icon svg-close"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
@@ -41,51 +47,89 @@
|
||||
<style lang='scss' scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
|
||||
small, strong {
|
||||
small {
|
||||
color: $white;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: $white;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
line-height: 1.714;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background-image: url('@/assets/images/g1g1-notif.png');
|
||||
background-image: url('@/assets/images/gifts_bg.svg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 10rem;
|
||||
padding: 3rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notification-remove {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 24px;
|
||||
top: 24px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
width: 5.75rem;
|
||||
min-height: 1.5rem;
|
||||
border-radius: 2px;
|
||||
border-color: $white;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
.gift-start {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.gift-end {
|
||||
height: 96px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scaleX(-1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.close-x {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
|
||||
&:hover .svg-close {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.svg-close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close-teal.svg?raw';
|
||||
import { mapActions } from '@/libs/store';
|
||||
import closeIcon from '@/assets/svg/close-white.svg?raw';
|
||||
|
||||
export default {
|
||||
props: ['notification'],
|
||||
props: ['notification', 'eventKey'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -94,11 +138,11 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
readNotification: 'notifications:readNotification',
|
||||
}),
|
||||
remove () {
|
||||
this.readNotification({ notificationId: this.notification.id });
|
||||
if (this.eventKey) {
|
||||
window.sessionStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
|
||||
}
|
||||
this.$emit('notification-removed');
|
||||
},
|
||||
showSelectUser () {
|
||||
this.$root.$emit('bv::show::modal', 'select-user-modal');
|
||||
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
props: ['notification', 'canRemove'],
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'achievements' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$router.push({ name: 'stats' });
|
||||
this.$router.push(`/profile/${this.$store.state.user.data._id}#stats`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
v-if="showOnboardingGuide"
|
||||
:never-seen="hasSpecialBadge"
|
||||
/>
|
||||
<gift-one-get-one-notification
|
||||
v-if="shouldShowG1g1"
|
||||
:notification="g1g1Notification"
|
||||
:event-key="g1g1EventKey"
|
||||
@notification-removed="handleG1g1Removed"
|
||||
/>
|
||||
<component
|
||||
:is="notification.type"
|
||||
v-for="notification in notifications"
|
||||
@@ -114,6 +120,7 @@
|
||||
<script>
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
|
||||
import find from 'lodash/find';
|
||||
import { mapState, mapActions } from '@/libs/store';
|
||||
import notificationsIcon from '@/assets/svg/notifications.svg?raw';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
@@ -151,6 +158,7 @@ export default {
|
||||
CARD_RECEIVED,
|
||||
CHALLENGE_INVITATION,
|
||||
GIFT_ONE_GET_ONE,
|
||||
GiftOneGetOneNotification: GIFT_ONE_GET_ONE,
|
||||
GROUP_TASK_ASSIGNED,
|
||||
GROUP_TASK_CLAIMED,
|
||||
GROUP_TASK_NEEDS_WORK,
|
||||
@@ -178,17 +186,14 @@ export default {
|
||||
hasSpecialBadge: false,
|
||||
quests,
|
||||
openStatus: undefined,
|
||||
g1g1Hidden: false,
|
||||
actionableNotifications: [
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION',
|
||||
],
|
||||
// A list of notifications handled by this component,
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
// NOTE: Those not listed here won't be shown in the notification panel!
|
||||
handledNotifications: [
|
||||
'NEW_STUFF',
|
||||
'ITEM_RECEIVED',
|
||||
'GIFT_ONE_GET_ONE',
|
||||
'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION',
|
||||
'PARTY_INVITATION',
|
||||
@@ -207,7 +212,10 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
notificationsOrder () {
|
||||
// Returns a map of NOTIFICATION_TYPE -> POSITION
|
||||
const orderMap = {};
|
||||
@@ -286,9 +294,9 @@ export default {
|
||||
|
||||
return notifications;
|
||||
},
|
||||
// The total number of notification, shown inside the dropdown
|
||||
notificationsCount () {
|
||||
return this.notifications.length;
|
||||
const g1g1Count = this.shouldShowG1g1 ? 1 : 0;
|
||||
return this.notifications.length + g1g1Count;
|
||||
},
|
||||
hasUnseenNotifications () {
|
||||
return this.notifications.some(notification => (notification.seen === false));
|
||||
@@ -299,6 +307,30 @@ export default {
|
||||
showOnboardingGuide () {
|
||||
return !hasCompletedOnboarding(this.user);
|
||||
},
|
||||
currentG1g1Event () {
|
||||
return find(this.currentEventList, event => event.promo === 'g1g1');
|
||||
},
|
||||
g1g1EventKey () {
|
||||
if (!this.currentG1g1Event || !this.currentG1g1Event.start) return null;
|
||||
const startDate = new Date(this.currentG1g1Event.start);
|
||||
return `${startDate.getFullYear()}-${startDate.getMonth()}`;
|
||||
},
|
||||
shouldShowG1g1 () {
|
||||
if (!this.currentG1g1Event) return false;
|
||||
const eventKey = this.g1g1EventKey;
|
||||
if (eventKey && window.sessionStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
|
||||
return false;
|
||||
}
|
||||
return !this.g1g1Hidden;
|
||||
},
|
||||
g1g1Notification () {
|
||||
return {
|
||||
type: 'GIFT_ONE_GET_ONE',
|
||||
id: `g1g1-event-${this.currentG1g1Event?.start || 'default'}`,
|
||||
data: {},
|
||||
seen: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const onboardingPanelState = getLocalSetting(CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE);
|
||||
@@ -364,6 +396,9 @@ export default {
|
||||
isActionable (notification) {
|
||||
return this.actionableNotifications.indexOf(notification.type) !== -1;
|
||||
},
|
||||
handleG1g1Removed () {
|
||||
this.g1g1Hidden = true;
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -176,7 +176,12 @@ export default {
|
||||
}
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$router.push({ name: startingPage });
|
||||
const userId = this.$store.state.user.data._id;
|
||||
let path = `/profile/${userId}`;
|
||||
if (startingPage !== 'profile') {
|
||||
path += `#${startingPage}`;
|
||||
}
|
||||
this.$router.push(path);
|
||||
},
|
||||
toLearnMore () {
|
||||
this.$router.push({ name: 'subscription' });
|
||||
|
||||
@@ -454,17 +454,14 @@ export default {
|
||||
},
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
|
||||
if (message.highlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { user } = this;
|
||||
const displayName = user.profile.name;
|
||||
const { username } = user.auth.local;
|
||||
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
|
||||
message.highlight = new RegExp(pattern, 'i').test(message.text);
|
||||
|
||||
if (!username) return false;
|
||||
const usernamePattern = new RegExp(`@${escapeRegExp(username)}(?:\\b|(?=[^a-zA-Z0-9_]))`, 'i');
|
||||
message.highlight = usernamePattern.test(message.text);
|
||||
return message.highlight;
|
||||
},
|
||||
flagCountDescription () {
|
||||
|
||||
@@ -197,9 +197,7 @@
|
||||
<select-translated-array
|
||||
:items="[
|
||||
'groupParentChildren',
|
||||
'groupCouple',
|
||||
'groupFriends',
|
||||
'groupCoworkers',
|
||||
'groupManager',
|
||||
'groupTeacher'
|
||||
]"
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
|
||||
z-index: 9;
|
||||
height: 3rem;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (max-width: 683px) {
|
||||
height: auto;
|
||||
min-height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
@@ -23,6 +29,19 @@
|
||||
padding: 0.75rem;
|
||||
|
||||
color: $gray-50;
|
||||
white-space: nowrap;
|
||||
|
||||
@media (max-width: 683px) {
|
||||
padding: 0.5rem;
|
||||
font-size: 13px;
|
||||
flex: 1 1 auto;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
padding: 0.5rem 0.4rem;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $purple-300;
|
||||
|
||||
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>
|
||||
@@ -12,14 +12,12 @@
|
||||
class="staff col-6 p-0"
|
||||
>
|
||||
<div class="d-flex">
|
||||
<router-link
|
||||
<div
|
||||
class="title"
|
||||
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
|
||||
>
|
||||
{{ user.name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.type === 'Staff'"
|
||||
class="svg-icon staff-icon ml-1"
|
||||
v-html="icons.tierStaff"
|
||||
></div>
|
||||
|
||||
@@ -269,7 +269,13 @@
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
max-width: calc(100vw - 20px);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-dialog {
|
||||
@@ -346,7 +352,23 @@
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
width: 448px;
|
||||
width: 100%;
|
||||
max-width: 448px;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrapper {
|
||||
@@ -564,7 +586,7 @@
|
||||
|
||||
.limitedTime {
|
||||
height: 32px;
|
||||
width: 446px;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
margin: 24px 0 0 0;
|
||||
background-color: $purple-300;
|
||||
@@ -851,7 +873,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
if (this.genericPurchase) {
|
||||
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
|
||||
await this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,22 @@
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
max-width: calc(100vw - 20px);
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
||||
@@ -163,8 +163,33 @@
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin-top: 8%;
|
||||
width: 448px !important;
|
||||
max-width: calc(100vw - 20px);
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 468px) {
|
||||
width: 100% !important;
|
||||
margin: 3rem auto 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 353px) {
|
||||
margin: 2.5rem auto 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-dialog {
|
||||
left: -8px;
|
||||
top: -8px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 300px) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -120,9 +120,9 @@
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
|
||||
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
|
||||
- Web Developer
|
||||
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
|
||||
({{ $t('commGuideOnGitHub', {gitHubName: 'phillipthelen'}) }})
|
||||
- Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
|
||||
@@ -133,10 +133,6 @@
|
||||
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
|
||||
- Art, Community Management, Many Hats
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
|
||||
- Web Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
|
||||
- Designer
|
||||
@@ -146,8 +142,12 @@
|
||||
- Mobile Designer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
|
||||
- Mobile Developer
|
||||
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Kalista'}) }}
|
||||
- Web Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'fizself', realName: 'Hafiz'}) }}
|
||||
- Developer
|
||||
</li>
|
||||
</ul>
|
||||
<p v-html="$t('commGuidePara013')"></p>
|
||||
@@ -156,7 +156,7 @@
|
||||
<em>
|
||||
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
|
||||
Breadstrings, Megan, Blade, Daniel the Bard, deilann, shanaqui, Nakonana,
|
||||
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
|
||||
Dewines, Alys, Fox_town, MaybeSteveRogers, Cantras, and heyeilatan.
|
||||
</em>
|
||||
</p>
|
||||
<h2 id="final">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,821 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable max-len -->
|
||||
<div class="container-fluid">
|
||||
<h1>HabitRPG Privacy Policy</h1>
|
||||
<p class="strong pagemeta">
|
||||
Last Updated September 1, 2025.
|
||||
</p>
|
||||
<p>
|
||||
This Privacy Policy applies when you interact with us through <a href="https://habitica.com">Habitica.com</a> (the “<strong>Site</strong>”), our mobile apps, and/or through any other feature or service owned or controlled by HabitRPG, Inc. ("<strong>HabitRPG</strong>", "<strong>we</strong>", or "<strong>us</strong>") that posts, links to, or references this Privacy Policy (collectively, the "<strong>Service(s)</strong>"). This Privacy Policy informs you of our practices regarding the collection, use, and disclosure of personal information we receive from users of our Services. By accessing or using the Services, you consent to our Privacy Policy and our collection, use, and disclosure of your information as described in this policy, our <a href="https://habitica.com/static/terms">Terms of Use</a>, and any additional policies and terms you may agree to in connection with the Services.
|
||||
</p>
|
||||
<h1>Table of Contents</h1>
|
||||
<ol>
|
||||
<li>
|
||||
<a href="#section_1">Collection of Information</a>
|
||||
<ol>
|
||||
<li><a href="#section_1_1">Information you Provide Directly</a></li>
|
||||
<li><a href="#section_1_2">Information We Collect Automatically</a></li>
|
||||
<li><a href="#section_1_3">Location Data</a></li>
|
||||
<li><a href="#section_1_2">Information Collected By and/or Disclosed to Third Parties</a></li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><a href="#section_2">Purpose and Use of Information We Collect</a></li>
|
||||
<li><a href="#section_3">Service Providers</a></li>
|
||||
<li><a href="#section_4">Sweepstakes, Contests, and Promotions</a></li>
|
||||
<li><a href="#section_5">Security</a></li>
|
||||
<li><a href="#section_6">Data Retention</a></li>
|
||||
<li><a href="#section_7">General Audience Services</a></li>
|
||||
<li><a href="#section_8">Consent to International Transfer</a></li>
|
||||
<li><a href="#section_9">Your Choices</a></li>
|
||||
<li><a href="#section_10">Changes to This Privacy Policy</a></li>
|
||||
<li><a href="#section_11">Jurisdiction-Specific Rights</a></li>
|
||||
<li><a href="#section_12">Contact Us</a></li>
|
||||
</ol>
|
||||
<h2 id="section_1">
|
||||
1. Collection of Information
|
||||
</h2>
|
||||
<p>
|
||||
We and our third-party service providers and business partners may collect information from you directly and/or automatically when you visit the Site or use the Services. We and our third-party service providers may also collect information about you from third parties. Some of this information may be considered "personal information" or "personal data" under applicable laws (collectively, “personal information”). We consider information that identifies you as a specific, identified individual (such as your name, phone number, and email address) to be personal information. We also, where required by applicable law, consider additional types of identifying data, like IP addresses and cookie identifiers, to be personal information.
|
||||
</p>
|
||||
<p>
|
||||
We may, in accordance with applicable law, take your personal information and de-identify or pseudonymize it to make it non-personally identifiable, either by combining it with information about other individuals and/or by hashing the information or otherwise removing characteristics that make the information personally identifiable. We maintain and use de-identified or pseudonymized data without attempting to re-identify it, except where permitted by applicable law, such as to determine whether our de-identification processes satisfy legal requirements. We will treat de-identified or pseudonymized information as non-personal to the fullest extent allowed by applicable law.
|
||||
</p>
|
||||
<p>
|
||||
We may collect the following categories of personal information from or about you:
|
||||
</p>
|
||||
<p>
|
||||
<u>Identifiers and Contact Information</u>. This category includes names, addresses, telephone numbers, mobile numbers, email addresses, signatures, account names, dates of birth, bank account information, and other similar contact information and identifiers.
|
||||
</p>
|
||||
<p>
|
||||
<u>Commercial Information</u>. This category includes, without limitation, products and services purchased, obtained, or considered, or other purchasing or consuming histories or tendencies.
|
||||
</p>
|
||||
<p>
|
||||
<u>Internet or Other Electronic Network Activity Information</u>. This category includes, without limitation, browsing history, search history, or a consumer’s interactions with a website, application, or advertisement.
|
||||
</p>
|
||||
<p>
|
||||
<u>Geolocation Data</u>. This category includes, without limitation, location information collected when using our Services.
|
||||
</p>
|
||||
<p>
|
||||
<u>Sensitive Personal Information</u>. This category includes:
|
||||
</p>
|
||||
<ul>
|
||||
<li>racial or ethnic origin, citizenship or immigration status, or religious beliefs;</li>
|
||||
<li>mental or physical health diagnosis;</li>
|
||||
<li>sexual orientation; and</li>
|
||||
<li>information collected from a known child.</li>
|
||||
</ul>
|
||||
<p><strong>
|
||||
NOTE: Please do not provide us “sensitive personal information” or “sensitive personal data”, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
|
||||
</strong></p>
|
||||
<h3 id="section_1_1">
|
||||
1.1 Information You Provide Directly
|
||||
</h3>
|
||||
<p>
|
||||
We may ask you to provide certain personal information when you use the Services. This information may include contact information (such as your name and email), account information (such as your email address and, if you choose to log in through Google or Apple, the associated user ID and email address), transaction information (such as your billing address and mailing address), or user content you choose to upload (such as photos and task lists). Note that all payments are handled by our third-party payment providers who may collect relevant information in order to complete your transaction (such as your payment card, billing address, and phone number). Our current providers are Apple, Google, PayPal, and Stripe. We encourage you to review their respective privacy policies for additional information regarding their privacy practices.
|
||||
</p>
|
||||
<p>
|
||||
We may also ask you to provide the contact information of another individual, such as when you invite another user to the Services. By providing us this information, you represent to us that you do so with the consent of the individual to whom it relates. We will only use this information for the specific reason for which it was provided.
|
||||
</p>
|
||||
<h3 id="section_1_2">
|
||||
1.2 Information We Collect Automatically
|
||||
</h3>
|
||||
<p>
|
||||
We and third-party companies and business partners may use a variety of technologies, including, without limitation, cookies, pixels, embedded scripts, and session events (collectively, “cookies”) that automatically or passively collect certain information whenever you visit our Site, use our Services, or otherwise interact with us or our content (“<strong>Usage Information</strong>”). Usage Information may include the hardware model, browser, and operating system you are using, the URL or advertisement that referred you to the Site you are visiting or Service you are using, all of the areas within the Site that you visit or Services that you use, your time zone, non-precise location information, and mobile network (if applicable), among other information. In addition, we automatically collect your IP address or other unique identifier ("<strong>Device Identifier</strong>") for any computer, mobile phone or other device you use to access our Services. In some cases, we may directly collect location information through your device. You may be able to turn off the collection of location information through the settings on your device. Usage Information is generally non-identifying, but if HabitRPG associates it with you as a specific and identifiable person, HabitRPG treats it as personal information.
|
||||
</p>
|
||||
<h4>Types of Cookies</h4>
|
||||
<ul>
|
||||
<li><u>First and Third-Party Cookies</u>. First party cookies are generally placed on your computer or device by the website you are visiting. For example, we may use a first party cookie to improve Site security. Third-party cookies are placed on your computer or device by a source other than the website you are visiting in order to enable third-party features such as advertising, analytics, videos, or interactive content.</li>
|
||||
<li><u>Essential or Strictly Necessary Cookies</u>. These cookies are necessary for the Site to function and cannot be switched off in our systems. They are usually set in response to actions taken by you which amount to a request for Services, such as setting your privacy preferences, logging in, or filling in forms.</li>
|
||||
<li><u>Performance and Functionality Cookies</u>. Although these are non-essential cookies, they help the Site perform and function as designed. For example, performance and functionality cookies may help the Site display videos, enable chat sessions, or recognize whether you visited the Site before.</li>
|
||||
<li><u>Analytics Cookies</u>. These cookies track your usage of the Site. The information these cookies collect can be used for various purposes such as understanding how visitors use the Site, Site and content customization, or advertising and marketing.</li>
|
||||
<li><u>Advertising Cookies</u>. Advertising and marketing cookies perform functions such as helping customize your Site experience, personalizing ads based on your online activities and interests, measuring the effectiveness of ads, preventing an ad from reappearing, and serving you targeted advertisements. Information from these cookies may be disclosed to third parties or third parties may place these cookies on your computer or device. Advertising and marketing cookies may track your online activities.</li>
|
||||
</ul>
|
||||
<p>
|
||||
The Site uses essential, functional, and performance cookies to function and perform as designed; analytics cookies to understand how you use the Site, improve its functionality, and other related purposes; and advertising cookies to help us with our advertising and marketing activities. We and our third-party partners and service providers may collect and track information about your online activities over time and across different websites, applications, and devices.
|
||||
</p>
|
||||
<h4>Google Analytics</h4>
|
||||
<p>
|
||||
We use Google Analytics, a service which uses cookies to collect and analyze data about the use of the Services and report on activities and trends. This service may also collect data about the use of other websites, apps, and online services. You can <a
|
||||
href="https://policies.google.com/technologies/partner-sites"
|
||||
target="_blank"
|
||||
>learn about</a> Google's practices, and opt out of them, by downloading the <a
|
||||
href="https://tools.google.com/dlpage/gaoptout"
|
||||
target="_blank"
|
||||
>Google Analytics opt-out browser add-on</a>.
|
||||
</p>
|
||||
<h4>Controlling Cookies</h4>
|
||||
<p>
|
||||
You may control cookies, including preventing or stopping the installation and storage of cookies, through your browser settings and other tools. Most browsers will allow you to block or refuse cookies. However, you may need to manually adjust your preferences each time you visit a site. For more information, see the Help section of your browser. Please note that if you block certain cookies, some of the services and functionalities of our Site may not work.
|
||||
</p>
|
||||
<p class="important">
|
||||
IMPORTANT: BY USING THE SITE, YOU CONSENT TO THE PROCESSING OF ANY PERSONAL INFORMATION FOR THE PURPOSES AND FUNCTIONS DESCRIBED ABOVE.
|
||||
</p>
|
||||
<h4>Do Not Track</h4>
|
||||
<p>
|
||||
“Do Not Track” is a privacy preference that you can set in your Internet search browser that sends a signal to a website that you do not want the website operator to track certain browsing information about you. However, because our Site is not configured to detect Do Not Track signals from a user’s computer, we are unable to respond to Do Not Track requests.
|
||||
</p>
|
||||
<h3 id="section_1_3">
|
||||
1.3 Location Data
|
||||
</h3>
|
||||
<p>
|
||||
We do not collect your precise location. However, please note that we may still be able to collect or infer your approximate location through other information we collect, such as IP address. In addition, some mobile service providers may also provide us or our third-party service providers with information regarding the non-precise physical location of the device you use to access our Services.
|
||||
</p>
|
||||
<h3 id="section_1_4">
|
||||
1.4 Information Collected By and/or Disclosed to Third Parties
|
||||
</h3>
|
||||
<p>
|
||||
We may receive information about you from third parties. For example, you may have the opportunity to log in through or otherwise connect your Apple and Google accounts. Additionally, when you interact with us through social media, you will be choosing to share information about your interactions with HabitRPG with that social media service.
|
||||
</p>
|
||||
<p>
|
||||
The following chart sets out by category the personal information we may collect ("<strong>Category</strong>"), the purposes for which we may collect it ("<strong>A. Purposes</strong>"), the categories of third parties to which we may disclose it for business purposes ("<strong>B. Disclosed To</strong>"), and the categories of third parties to which we may sell it for monetary value or other valuable consideration or share it for cross-context behavioral advertising/targeted marketing ("<strong>C. Sold/Shared To</strong>")
|
||||
</p>
|
||||
|
||||
<button
|
||||
for="showThirdParties"
|
||||
class="btn btn-primary"
|
||||
@click="toggleThirdPartyInfo"
|
||||
>
|
||||
{{ showThirdPartyInfo ? "Hide Information from Third Parties" : "Show Information from Third Parties" }}
|
||||
</button>
|
||||
<div v-if="showThirdPartyInfo">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Contact Information</strong> (such as your name, email address, mailing address)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to process and fulfill your purchases of products; enable you to use the Services and other products or services we offer, including maintaining your account information and verifying information you provide to us, such as that your email address is active and valid; send you transactional messages regarding your use of the Services, your purchases, and other account-related communications; respond to your inquiries and provide customer support or request feedback from you); <strong>(2) Advertising Activities and Marketing Messages</strong> (to customize and optimize content for you, such as to send you marketing communications if you have provided us your email address, as permitted by applicable law); <strong>(3) Administrative</strong> (to verify your identity, recognize you across the Services and your devices; contact you regarding your use of the Services, content, features, or products you use or request, and, in our discretion, changes to our policies); <strong>(4) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts through our third-party service providers; detect and troubleshoot problems; monetize our Services); <strong>(5) Security</strong> (to protect integrity of the Services); <strong>(6) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(7) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(8) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Analytics Partners (such as analytics companies); Social Media Platforms; Business Partners; Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as-a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers (such as email delivery and direct mail vendors); Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
Analytics Partners.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>User Account Information</strong> (such as account name, communication preferences, and account settings)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to process your purchases; to enable you to use the Services and other products or services we offer, including maintaining your account information and verifying information you provide to us, such as that your email address is active and valid; send you transactional messages regarding your use of the Services and other account-related communications; respond to your inquiries and provide customer support or request feedback from you); <strong>(2) Advertising Activities and Marketing Messages</strong> (to customize and optimize content for you, such as to send you in-app announcements and marketing communications if you have provided us your email address, as permitted by applicable law); <strong>(3) Personalization</strong> (to tailor the content we display to you on the Services, including content in emails we may send to you, to tell you about new products, promotions, opportunities, or other general information about HabitRPG or our products that we believe will be of interest to you); <strong>(4) Administrative</strong> (to verify your identity, recognize you across the Services and your devices; contact you regarding your use of the Services, content, features, or products you use or request, and, in our discretion, changes to our policies); <strong>(5) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts; detect and troubleshoot problems; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(6) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(7) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(8) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(9) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by applicable law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software- as-a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
This category of data is not sold or shared for targeted advertising.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Demographic Information</strong> (such as your zip code, birth day/month)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to enable you to use the Services and other products or services we offer, including verifying information you provide to us); <strong>(2) Advertising Activities and Marketing Messages</strong> (to customize and optimize content for you); <strong>(3) Personalization</strong> (to tailor the content and advertising we display to you or others, on the Services or elsewhere, including content in emails we may send to you); <strong>(4) Administrative</strong> (to verify your identity, recognize you across the Services and your devices); <strong>(5) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts; detect and troubleshoot problems; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(6) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(7) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(8) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(9) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by applicable law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Social Media Platforms and Other Third Party Platforms; Third Party Business Partners; Order Fulfillment Vendors (such as payment processors); Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as-a-service providers, and technology maintenance and repair vendors); Security Vendors; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
This category of data is not sold or shared for targeted advertising.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Device ID/Interaction Information</strong> (such as IP address or other unique ID for any computer, mobile phone or other device used to access the Services, device information (model, browser, operating system version), URL or advertisement that referred you to the Services, any search terms you entered into a search engine that led you to the Services; areas within Services that you visit, time of day you visited the Services, your time zone, location information, and mobile network (if applicable) information)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to process and fulfill your purchases of products; to enable you to use the Services and other products or services we offer, including maintaining your account information and verifying information you provide to us, such as that your email address is active and valid; send you transactional messages regarding your use of the Services and other account-related communications); <strong>(2) Advertising Activities and Marketing Messages</strong> (to customize and optimize content for you, such as to send you in-app announcements and marketing communications if you have provided us your email address, as permitted by applicable law); <strong>(3) Personalization</strong> (to tailor the content and advertising we display to you or others, on the Services or elsewhere, including content in emails we may send to you, to tell you about new products, promotions, opportunities, or other general information about HabitRPG or our products that we believe will be of interest to you); <strong>(4) Administrative</strong> (to verify your identity, recognize you across the Services and your devices; contact you regarding your use of the Services, content, features, or products you use or request, and, in our discretion, changes to our policies); <strong>(5) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts; detect and troubleshoot problems; monetize our Services; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(6) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(7) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(8) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(9) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Analytics Partners; Social Media Platforms and Other Third Party Platforms; Third Party Business Partners; Order Fulfillment Vendors (such as payment processors, fulfillment centers, delivery services, order tracking vendors); Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as- a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
Analytics Partners.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Transaction Information</strong> (such as data about the products you purchase, obtained, or considered, customer service contacts)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to enable you to use the Services and other products or services we offer, including maintaining your account information and verifying information you provide to us; send you transactional messages regarding your use of the Services, your purchases, and other account-related communications; respond to your inquiries and provide customer support or request feedback from you); <strong>(2) Advertising Activities and Marketing Messages</strong> (to customize and optimize content for you, such as to send you in-app announcements and marketing communications if you have provided us your email address, as permitted by applicable law); <strong>(3) Personalization</strong> (to tailor the content we display to you on the Services, including content in emails we may send to you, to tell you about new products, promotions, opportunities, or other general information about HabitRPG or our products that we believe will be of interest to you); <strong>(4) Administrative</strong> (to verify your identity, recognize you across the Services and your devices; contact you regarding your use of the Services, content, features, or products you use or request, and, in our discretion, changes to our policies); <strong>(5) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts; detect and troubleshoot problems; monetize our Services; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(6) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(7) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(8) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(9) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by applicable law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Order Fulfillment Vendors (such as payment processors, fulfillment centers, delivery services, order tracking vendors); Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as- a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
This category of data is not sold or shared for targeted advertising.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Information about your Interests and Preferences</strong> (such as information about the products you purchased, obtained, or considered)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to enable you to use the Services and other products or services we offer, including maintaining your account information and verifying information you provide to us; send you transactional messages regarding your use of the Services and other account-related communications; respond to your inquiries and provide customer support or request feedback from you); <strong>(2) Personalization</strong> (to tailor the content we display to you or others, on the Services or elsewhere, including content in emails we may send to you, to tell you about new products, promotions, opportunities, or other general information about HabitRPG or our products that we believe will be of interest to you); <strong>(3) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; analyze the effectiveness of our marketing efforts; detect and troubleshoot problems; monetize our Services; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(4) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(5) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(6) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(7) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by applicable law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Social Media Platforms and Other Third Party Platforms; Third Party Business Partners; Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as-a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
This category of data is not sold or shared for targeted advertising.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
rowspan="3"
|
||||
class="first-column"
|
||||
>
|
||||
Category of Data
|
||||
</th>
|
||||
<th>A. Purpose for Collection;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>B. Categories of Third Parties to which Personal Information may be Disclosed;</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>C. Categories of Third Parties to which the Personal Information may be Sold/Shared for Targeted Advertising</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td rowspan="3">
|
||||
<strong>Photo/Video Data, and Other User Generated Content</strong> (such as tasks, profile, chat messages)
|
||||
</td>
|
||||
<td>
|
||||
<h6>A. Purposes for Collection:</h6>
|
||||
<p>
|
||||
<strong>(1) Provide You the Service</strong> (to enable you to use the Services and other products or services we offer, including maintaining your account information and permitting you to create to do lists and upload related content; send you transactional messages regarding your use of the Services and other account-related communications; respond to your inquiries and provide customer support or request feedback from you); <strong>(2) Administrative</strong> (to verify your identity, recognize you across the Services and your devices; contact you regarding your use of the Services, content, features, or products you use or request, and, in our discretion, changes to our policies); <strong>(3) Internal Business Purposes</strong> (to operate and improve the Services and our products, services, and marketing endeavors; market research; detect and troubleshoot problems; monetize our Services; understand how you use the Services including tracking traffic, usage, trends, and navigation patterns); <strong>(4) Security</strong> (to protect integrity of the Services; to investigate, prevent, and detect, and protect against misuse of our systems, abuse, fraud or other crime, or illegal activities or those that violate our policies; to detect and troubleshoot problems); <strong>(5) Legal</strong> (to comply with the law; resolve disputes; enforce our agreements and policies; cooperate in governmental or other legal inquiries; fulfill regulatory reporting obligations; protect our or others' rights, assets, safety or security); <strong>(6) Business Transitions</strong> (in connection with a merger, acquisition, consolidation, bankruptcy, or other corporate transition, including during due diligence); <strong>(7) Other Purposes</strong> as disclosed at the time of collection, with your consent (where required by applicable law), or as permitted by applicable law.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>B. Categories of Third Parties to which Personal Information may be Disclosed:</h6>
|
||||
<p>
|
||||
Third Party Business Partners; Technology Systems Service Providers (such as webhosts, database hosts, cloud computing providers, software-as-a-service providers, and technology maintenance and repair vendors); Security Vendors; Communications Service Providers; Customer Service Providers; Government Agencies and Law Enforcement/Where Required by Law (including third parties in connection with a court or other legal proceeding); Third Parties in connection with court or other legal actions; Third Parties in connection with business transitions; Third Parties to whom you agree or direct us to share your data; Other Service Providers we engage to provide services to us and/or you.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>C. Categories of Third Parties to which Personal Information may be Sold/Shared for Targeted Advertising:</h6>
|
||||
<p>
|
||||
This category of data is not sold or shared for targeted advertising.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="section_2">
|
||||
2. Purpose and Use of Information We Collect
|
||||
</h2>
|
||||
<p>
|
||||
We may use non-personal information for any purpose, including for research and marketing purposes. We also use information that we collect, including personal information and Usage Information, as disclosed in this Privacy Policy and as follows:
|
||||
</p>
|
||||
<ul>
|
||||
<li>to provide the Services to you and allow you to use the features we offer;</li>
|
||||
<li>to verify your identity and to otherwise manage your user account;</li>
|
||||
<li>to tailor and target content, recommendations, and offers we display to you via the Services;</li>
|
||||
<li>to send you communications with information about our products and Services;</li>
|
||||
<li>to fulfill your order, send you an order confirmation, process your payment, and communicate with you about your order;</li>
|
||||
<li>to respond to your inquiries, customer service questions, feedback, or requests;</li>
|
||||
<li>to provide you with technical support;</li>
|
||||
<li>to improve our Services and for legal, regulatory, and internal business purposes; and</li>
|
||||
<li>to fulfill any other purpose consistent with this Privacy Policy.</li>
|
||||
</ul>
|
||||
<p>
|
||||
We may also use your personal information for any other purpose disclosed to you at the time of collection, with your consent (where required by applicable law), or permitted by applicable law.
|
||||
</p>
|
||||
|
||||
<h2 id="section_3">
|
||||
3. Service Providers
|
||||
</h2>
|
||||
<p>
|
||||
Our service providers may collect information on our behalf and at our direction, in order to provide Services on our behalf to help with our business activities.
|
||||
</p>
|
||||
<p>
|
||||
These companies are authorized to use your personal information only as necessary to provide these Services to us and for other purposes permitted by applicable law.
|
||||
</p>
|
||||
<p>
|
||||
Our Service Providers include, without limitation:
|
||||
</p>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<th>
|
||||
Service Provider Name
|
||||
</th>
|
||||
<th>
|
||||
Product(s)
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Google Cloud</td>
|
||||
<td>cloud computing; storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MongoDB</td>
|
||||
<td>database</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Heroku</td>
|
||||
<td>cloud-based testing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Amazon Web Services</td>
|
||||
<td>content storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hetzner</td>
|
||||
<td>translations and push notifications</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stripe</td>
|
||||
<td>payment processing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PayPal</td>
|
||||
<td>payment processing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Amazon Payments</td>
|
||||
<td>payment processing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Apple App Store</td>
|
||||
<td>app host</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Google Play Store</td>
|
||||
<td>app host</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mailchimp</td>
|
||||
<td>email marketing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gmail</td>
|
||||
<td>internal communications</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redislabs</td>
|
||||
<td>rate limiting</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Loggly</td>
|
||||
<td>log management and analytics</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Slack</td>
|
||||
<td>internal communications</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Amplitude</td>
|
||||
<td>analytics</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2 id="section_4">
|
||||
4. Sweepstakes, Contents and Promotions
|
||||
</h2>
|
||||
<p>
|
||||
We may offer sweepstakes, contests, or other promotions (collectively, "<strong>Promotion(s)</strong>") that may require registration. By participating in a Promotion, you agree to the provisions, conditions, or official rules that govern the Promotion, which may contain specific requirements of you (including, except where prohibited by law, allowing the sponsor(s) of the Promotion to use your name, voice, likeness, or other indicia of persona in advertising or marketing materials). If you choose to enter a Promotion, personal information may be disclosed to co-promotion partners, third parties or the public in connection with the administration of such Promotion, including in connection with winner selection, prize fulfillment, as required by law, or as permitted by the Promotion's terms or official rules.
|
||||
</p>
|
||||
|
||||
<h2 id="section_5">
|
||||
5. Security
|
||||
</h2>
|
||||
<p>
|
||||
HabitRPG maintains commercially reasonable administrative, physical, and technical safeguards that are designed to secure your personal information; however, no data transmission over the Internet, wireless transmission, or electronic storage of data can be guaranteed to be 100% secure. HabitRPG cannot ensure or warrant the security of any data we collect. You use the Services and provide us your data at your own risk.
|
||||
</p>
|
||||
|
||||
<h2 id="section_6">
|
||||
6. Data Retention
|
||||
</h2>
|
||||
<p>
|
||||
We retain your personal information for as long as necessary to provide you Services and in accordance with our data retention schedule. We may retain your personal information for longer if it is necessary to comply with our legal or reporting obligations, resolve disputes, enforce contracts, or address other legitimate business needs, or as permitted or required by applicable law. We may also retain your personal information in a deidentified or aggregated form so that it can no longer be associated with you. To determine the appropriate retention period for your personal information, we consider various factors, such as the amount, nature, and sensitivity of your information; the potential risk of unauthorized access, use, or disclosure; the purposes for which we collect or process your personal information; and applicable legal requirements.
|
||||
</p>
|
||||
|
||||
<h2 id="section_7">
|
||||
7. General Audience Services
|
||||
</h2>
|
||||
<p>
|
||||
The Service are intended for users 13 years or older; you are not permitted to access or use the Service if you are younger than 13. We do not knowingly collect personal information from children under the age of 13 through the Service. We encourage parents and legal guardians to monitor their children's Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 13 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
|
||||
</p>
|
||||
|
||||
<h2 id="section_8">
|
||||
8. Consent to International Transfer
|
||||
</h2>
|
||||
<p>
|
||||
HabitRPG is based in the United States. Please be aware that information we collect will be transferred to and processed in the United States and other countries. HabitRPG makes no representation that this Privacy Policy or the practices described in it comply with the laws of any other jurisdiction. By using the Services, or providing us with any information, you fully understand and unambiguously consent to this transfer, processing, and storage of your information in the United States and other jurisdictions for which the privacy laws may not be as comprehensive as those in the country where you reside and/or are a citizen. As a result, this information may be subject to access requests from governments, courts, or law enforcement in the United States and other countries according to laws in those jurisdictions.
|
||||
</p>
|
||||
|
||||
<h2 id="section_9">
|
||||
9. Your Choices
|
||||
</h2>
|
||||
<p>
|
||||
<em>Edit Your Information</em>: On the website, you can update the information in your user profile at any time by going to the user icon in the upper right, selecting the "Profile" option, then clicking "Edit Profile". You can update your username and email address by going to the user icon in the upper right and selecting the "Settings" option. If you are using the mobile app, you can update your user profile, username, and email by tapping the Settings gear in the menu and then selecting the "My Account" option.
|
||||
</p>
|
||||
<p>
|
||||
<em>Reset Your Account</em>: You can fully delete or reset your account by selecting the user icon in the upper right, selecting the "Settings", and then looking under "General Settings". You can fully delete or reset your account on the mobile apps by tapping the Settings gear in the menu and then selecting the "My Account" submenu. Please note that in order to fully delete all data associated with your account, you will need to email us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. Note that we may be required to retain certain data about you to comply with applicable laws.
|
||||
</p>
|
||||
<p>
|
||||
<em>Newsletter</em>: You may also sign-up to receive our email newsletter. If you would like to discontinue receiving this information, you may update your email preferences by using the "Unsubscribe" link found in emails we send to you, or by contacting us. Please note that we reserve the right to send you certain communications relating to your account or use of the Services, such as administrative and services announcements. These transactional account messages may be unaffected if you choose to opt out from marketing emails.
|
||||
</p>
|
||||
<p>
|
||||
<em>Push Notifications</em>: With your consent, we may send promotional and non-promotional push notifications or alerts to your mobile device. You can elect to stop receiving those messages by changing the notification settings in the app or on your mobile device.
|
||||
</p>
|
||||
<p>
|
||||
<em>Other Privacy Rights</em>: Certain jurisdictions provide additional rights. Please see the "<a href="#section_11">Jurisdiction-Specific Rights</a>" section below for more information.
|
||||
</p>
|
||||
|
||||
<h2 id="section_10">
|
||||
10. Changes to This Privacy Policy
|
||||
</h2>
|
||||
<p>
|
||||
To the extent permitted by applicable law, we reserve the right to change or modify this Privacy Policy at our discretion at any time. We will notify you of any material changes by posting the changed or modified Privacy Policy on our Services. We may also provide notice to you in other ways, such as through contact information you have provided. Any changes will be effective immediately upon the posting of the revised Privacy Policy unless otherwise specified. Your continued use of the Services after the effective date of the revised Privacy Policy (or such other act as specified in the revised Privacy Policy) will, to the fullest extent permitted by applicable law, constitute your consent to those changes. However, we will provide notice and obtain your consent (opt-in or opt-out) if required by law. We encourage you to regularly review this Privacy Policy for the latest information on our privacy practices.
|
||||
</p>
|
||||
|
||||
<h2 id="section_11">
|
||||
11. Jurisdiction-Specific Rights
|
||||
</h2>
|
||||
<p><strong><u>California, Nebraska, and Texas Residents</u></strong></p>
|
||||
<p>
|
||||
This section applies only to California, Nebraska, and Texas residents. It supplements and amends the information contained in the Policy with respect to such individuals. The other provisions of the Policy continue to apply, except as modified in this section.
|
||||
</p>
|
||||
<p>
|
||||
<em>Shine the Light [California residents only]</em>. California Civil Code Section 1798.83 permits you to request information regarding the disclosure of your personal information by us to third parties for the third parties’ direct marketing purposes. Such requests must be submitted to us in accordance with the instructions in the Contact Us section of this Policy. Please mention when contacting us that you are making a “California Shine the Light” inquiry. Within 30 days of receiving such a request, we will provide a list of the categories of personal information disclosed to third parties for third-party direct marketing purposes during the immediately preceding calendar year, along with the names and addresses of these third parties. This request may be made no more than once per calendar year. We reserve our right not to respond to requests submitted other than in accordance with the instructions specified in this paragraph.
|
||||
</p>
|
||||
<p>
|
||||
<em>Eraser Law [California residents only]</em>. If you are a California resident under the age of 18, and a registered user of any site where this policy is posted, California law permits you to request and obtain removal of content or information you have publicly posted. You may submit your request using the contact information at the end of this Policy. Please be aware that such request does not ensure complete or comprehensive removal of the content or information you have posted and that there may be circumstances in which the law does not require or allow removal even if requested.
|
||||
</p>
|
||||
<p>
|
||||
<u><em>Your Rights</em></u>
|
||||
</p>
|
||||
<p>
|
||||
You may have certain rights related to your personal information, subject to certain exceptions (including, as applicable and without limitation, safety, security, and protecting the rights of other users). Specifically:
|
||||
</p>
|
||||
<p>
|
||||
<em>Right to Confirm and Access</em>. You may have the right to request that we confirm whether we process your personal information and, if we do, that we grant you access to that information. If you previously provided us your personal information, and that information is available in a digital format, you may have the right to obtain a copy of the information in a portable and, to the extent technically feasible, readily usable format that allows you to transmit the information to another person without hindrance.
|
||||
</p>
|
||||
<p>
|
||||
<em>Right to Delete</em>. You may have the right to request that we delete your personal information from our records, subject to certain exceptions.
|
||||
</p>
|
||||
<p>
|
||||
<em>Right to Correct</em>. You may have the right, subject to certain limitations, to request that we correct any inaccurate personal information we maintain about you.
|
||||
</p>
|
||||
<p>
|
||||
<em>Opt - Out of the “ Sale” of Personal Information or Use of Such Information for “Targeted Advertising” or “Profiling”</em>. We engage in common marketing and advertising practices to provide more relevant content and ads to users of our Site and Services. Certain of these practices may involve the “selling” of personal information, or the use of such information for “targeted advertising” or “profiling,” as those terms are defined in the Texas Data Privacy and Security Act (“TDPSA”) and the Nebraska Data Privacy Act. We do not sell personal information under the more commonly understood meaning of that word—i.e., providing personal information to third parties in exchange for money. Nor do we have actual knowledge of selling personal information of minors under the age of 16. To opt-out of the selling of your personal information, or use of that information for targeted advertising or profiling, please submit a request to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. Note: We also treat Global Privacy Control browser signals as opt-out of sale/disclosure for targeted advertising or profiling requests. To opt-out via the Global Privacy Control, please follow the instructions available <a
|
||||
href="https://globalprivacycontrol.org/"
|
||||
target="_blank"
|
||||
>here</a>.
|
||||
</p>
|
||||
<p>
|
||||
<em>Right to Consent to Use or Disclosure of Sensitive Personal Information</em>. Where required by applicable law, we will process your sensitive personal information only with your consent.
|
||||
</p>
|
||||
<p>
|
||||
<em>Right Against Discrimination</em>. You have the right not to be discriminated against for exercising any of the rights described in this section. For example, we generally will not provide you a different level or quality of goods or services if you exercise these rights.
|
||||
</p>
|
||||
<p>
|
||||
<em>Submitting Data Subject Rights Requests</em>. To submit a data subject rights request, please email us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. We reserve the right to only respond to verifiable requests. To verify your identity, we may ask you to verify personal information we already have on file for you. If we cannot verify your identity from the information we have on file, we may request additional information from you, which we will only use to verify your identity, and for security or fraud-prevention purposes. You will need to describe your request with sufficient detail to allow us to review, understand, assess, and respond to it, and we may not be able to respond to your request, or provide you with personal information, if we cannot verify your identity or authority to make the request and confirm the personal information relates to you. You may authorize another person to act on your behalf with respect to your rights under this section. We reserve the right to deny requests from persons claiming to be authorized agents that do not submit sufficient proof of their authorization.
|
||||
</p>
|
||||
<p>
|
||||
<em>Our Response</em> . You may have the right, subject to applicable law, to submit up to two (2) requests per year free of charge. We reserve the right to charge a fee to process or respond to your request if it is excessive, repetitive, or manifestly unfounded. If we determine that a request warrants a fee, we will attempt to notify you as to why we made that decision and provide a cost estimate before completing your request. We will endeavor to respond to verifiable requests within forty-five (45) calendar days of receipt, but we may require an extension of up to forty-five (45) additional calendar days to respond and we will notify you of the need for the extension.
|
||||
</p>
|
||||
<p>
|
||||
If you have an account with us, we will deliver our written response to that account. If you do not have an account with us, we will deliver our written response by email. In the event we deny your request, in whole or in part, we will explain the bases for that denial.
|
||||
</p>
|
||||
<p>
|
||||
<em>Appeal of Our Response</em>. In the event you believe we have erroneously denied your request, in full or in part, you may, within 60 days of receipt of that denial, submit an appeal to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. In your appeal submission, please explain why you believe our decision to deny your request was incorrect and please provide any additional information you believe we should consider in connection with your appeal. Within 60 days of receipt of your appeal, we will advise you in writing of any action we have taken, or refrained from taking, in response to your appeal, along with an explanation of why we have taken, or refrained from taking, such action.
|
||||
</p>
|
||||
|
||||
<p><strong><u>Nevada Residents</u></strong></p>
|
||||
<p>
|
||||
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
|
||||
</p>
|
||||
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
|
||||
<p>
|
||||
If you are a resident of the United Kingdom (UK), European Economic Area (EEA), or of Switzerland, the following information applies.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Purposes of processing and legal basis for processing:</strong> As explained above, we process personal information in various ways depending upon your use of our Services. We process personal information on the following legal bases: (1) with your consent; (2) as necessary to perform our agreement to provide the Services; (3) compliance with our legal obligations; and (4) as necessary for our legitimate interests in providing the Service where those interests do not override your fundamental rights and freedoms related to data privacy such as for:
|
||||
<ul>
|
||||
<li>preventing fraud;</li>
|
||||
<li>ensuring network and information security, including preventing unauthorized access to our computer and electronic communication systems and preventing malicious software distribution;</li>
|
||||
<li>supporting internal administration;</li>
|
||||
<li>improving and developing the Services; and</li>
|
||||
<li>conducting data analytics analyses to review and better understand consumer interaction.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Right to lodge a complaint:</strong> Users that reside in the UK, EEA, or Switzerland have the right to seek information and assistance or lodge a complaint about our data collection and processing actions with the supervisory authority where they reside. Contact details for data protection authorities are available here. UK: <a
|
||||
href="https://ico.org.uk/"
|
||||
target="_blank"
|
||||
>https://ico.org.uk/</a> EEA: <a
|
||||
href="https://edpb.europa.eu/about-edpb/board/members_en"
|
||||
target="_blank"
|
||||
>https://edpb.europa.eu/about-edpb/board/members_en</a> Switzerland: <a
|
||||
href="https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html"
|
||||
target="_blank"
|
||||
>https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html</a>.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Transfers:</strong> Personal information we collect may be transferred to, and stored and processed in, the United States or any other country in which we or our affiliates or subcontractors maintain facilities. Transfers of personal data to a third country without an adequacy decision (as that term is understood pursuant to Article 45 of GDPR) are required to be subject to appropriate safeguards such as standard contractual clauses. In certain cases, we rely on your consent to facilitate transfer, processing, and storage of your data in the United States and other jurisdictions, where laws regarding processing of personal information may be less stringent than the laws in the EEA, UK, and Switzerland.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Withdraw consent:</strong> If we have collected personal information with your consent, you have the right to withdraw that consent at any time.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Access:</strong> You have the right to request access to personal information we collected about you and information about its sources, purposes, and sharing.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Correction:</strong> You have the right to request that we correct the personal information we hold about you if it is inaccurate or incomplete.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Erasure:</strong> You have the right to request that we erase data we have collected from you. Please note that we may have a reason to deny your deletion request or delete data in a more limited way than you anticipated, e.g., because of a legal obligation to retain it.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Portability:</strong> You have the right, in certain circumstances, to request that we provide your personal information to you in a format that can be transferred to another entity.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Restrict Processing:</strong> You have the right, in certain circumstances, to request that we limit our processing of your personal information if you are (1) contesting the accuracy of your personal information, (2) asserting that our processing is unlawful; (3) asserting that we no longer need to keep the information for reasons related to the establishment, exercise, or defense of legal claims, or you object to our processing. You have the right, in certain circumstances, to request that we limit our processing of your personal information if you are contesting the accuracy of your personal information; asserting that our processing is unlawful; asserting that we no longer need to keep the information for reasons related to the establishment, exercise, or defense of legal claims, or you object to our processing
|
||||
</p>
|
||||
<p>
|
||||
<strong>Objection:</strong> You have the right to object to our processing if we are processing your personal information based on legitimate interests, using your personal information for direct marketing (including profiling), or processing your personal information for purposes of scientific or historical research and statistics.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Verification Procedures:</strong> We must verify your identity for everyone's protection, so we may require you to provide us with verification information prior to accessing any records containing personal information about you. We do this by asking you to provide personal identifiers we can match against information we may have collected from you previously and confirm your request using the email stated in the request.
|
||||
</p>
|
||||
<p>
|
||||
We will use the information you provide for verification only for the purpose of verification. We may have a reason under the law why we do not have to respond to your request or respond to it in a more limited way than you anticipated. If we do, we will explain that to you in our response.
|
||||
</p>
|
||||
|
||||
<h2 id="section_12">
|
||||
12. Contact Us
|
||||
</h2>
|
||||
<p>
|
||||
If you have any questions or concerns about this Privacy Policy, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a> with “Privacy Policy” in the subject line. You may also write to us at:
|
||||
</p>
|
||||
<address class="ml-4">
|
||||
HabitRPG, Inc. c/o Workbar<br>
|
||||
120 Washington Street Suite 202<br>
|
||||
Salem, MA 01970-6396
|
||||
</address>
|
||||
</div>
|
||||
<!-- eslint-enable max-len -->
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/assets/scss/colors.scss';
|
||||
h4 {
|
||||
text-decoration: underline;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.important {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.first-column {
|
||||
width: 25%
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PrivacyReview',
|
||||
data () {
|
||||
return {
|
||||
showThirdPartyInfo: true,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleThirdPartyInfo () {
|
||||
this.showThirdPartyInfo = !this.showThirdPartyInfo;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -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,23 @@
|
||||
<router-view />
|
||||
</div>
|
||||
<div
|
||||
id="bottom-background"
|
||||
v-if="loginFlow"
|
||||
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 id="bottom-background" v-if="!loginFlow">
|
||||
<div class="seamless_mountains_demo_repeat"></div>
|
||||
<div class="midground_foreground_extended2"></div>
|
||||
</div>
|
||||
@@ -263,13 +269,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>
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
<template>
|
||||
<!-- eslint-disable max-len -->
|
||||
<div class="container-fluid">
|
||||
<h1>Terms of Service</h1>
|
||||
<p class="strong pagemeta">
|
||||
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"), 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 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 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>
|
||||
<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>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>
|
||||
<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).
|
||||
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>
|
||||
|
||||
<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 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, 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>
|
||||
<li>Abusing or submitting excessively frequent requests to the Service via the API</li>
|
||||
<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 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>
|
||||
|
||||
<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>
|
||||
<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 “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 <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. 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>
|
||||
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 REFUNDS FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
</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 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>
|
||||
|
||||
<h2>Binding Arbitration</h2>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 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, please contact us at <a href="mailto:admin@habitica.com">admin@habitica.com</a>.
|
||||
</p>
|
||||
|
||||
<h2>Miscellaneous</h2>
|
||||
<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
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary d-flex align-items-center justify-content-center"
|
||||
:class="{disabled: !canSave}"
|
||||
:class="{'btn-disabled': !canSave}"
|
||||
type="button"
|
||||
@click="submit()"
|
||||
>
|
||||
@@ -162,13 +162,13 @@
|
||||
>
|
||||
<div
|
||||
class="habit-option-icon svg-icon no-transition"
|
||||
:class="task.up ? '' : 'disabled'"
|
||||
:class="task.up ? '' : 'icon-disabled'"
|
||||
v-html="icons.positive"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="habit-option-label no-transition"
|
||||
:class="task.up ? cssClass('icon') : 'disabled'"
|
||||
:class="task.up ? cssClass('icon') : 'label-disabled'"
|
||||
>
|
||||
{{ $t('positive') }}
|
||||
</div>
|
||||
@@ -188,13 +188,13 @@
|
||||
>
|
||||
<div
|
||||
class="habit-option-icon no-transition svg-icon negative mx-auto"
|
||||
:class="task.down ? '' : 'disabled'"
|
||||
:class="task.down ? '' : 'icon-disabled'"
|
||||
v-html="icons.negative"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="habit-option-label no-transition"
|
||||
:class="task.down ? cssClass('icon') : 'disabled'"
|
||||
:class="task.down ? cssClass('icon') : 'label-disabled'"
|
||||
>
|
||||
{{ $t('negative') }}
|
||||
</div>
|
||||
@@ -592,7 +592,7 @@
|
||||
<button
|
||||
class="btn btn-primary btn-footer
|
||||
d-flex align-items-center justify-content-center"
|
||||
:class="{disabled: !canSave}"
|
||||
:class="{'btn-disabled': !canSave}"
|
||||
type="button"
|
||||
@click="submit()"
|
||||
>
|
||||
@@ -881,12 +881,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
.btn-disabled {
|
||||
background-color: $white;
|
||||
border: 2px solid transparent;
|
||||
color: $gray-200;
|
||||
line-height: 1.714;
|
||||
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
|
||||
&:focus {
|
||||
background-color: $white;
|
||||
@@ -948,7 +950,7 @@
|
||||
height: 10px;
|
||||
color: $white;
|
||||
|
||||
&.disabled {
|
||||
&.icon-disabled {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
@@ -962,7 +964,7 @@
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
|
||||
&.disabled {
|
||||
&.label-disabled {
|
||||
color: $gray-100;
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -1018,7 +1020,7 @@
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.disabled .input-group-text {
|
||||
.input-group-outer.disabled .input-group-text {
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,17 @@
|
||||
:checked="isChecked"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<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"
|
||||
@@ -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 {
|
||||
|
||||
@@ -1126,7 +1126,12 @@ export default {
|
||||
this.loadUser();
|
||||
this.oldTitle = this.$store.state.title;
|
||||
this.handleExternalLinks();
|
||||
this.selectPage(this.startingPage);
|
||||
// Check if there's a hash in the URL to determine the starting page
|
||||
let pageToSelect = this.startingPage;
|
||||
if (window.location.hash && (window.location.hash === '#stats' || window.location.hash === '#achievements')) {
|
||||
pageToSelect = window.location.hash.substring(1);
|
||||
}
|
||||
this.selectPage(pageToSelect);
|
||||
this.$root.$on('habitica:report-profile-result', () => {
|
||||
this.loadUser();
|
||||
});
|
||||
@@ -1211,10 +1216,15 @@ export default {
|
||||
},
|
||||
selectPage (page) {
|
||||
this.selectedPage = page || 'profile';
|
||||
window.history.replaceState(null, null, '');
|
||||
const profileUserId = this.userId || this.userLoggedIn._id;
|
||||
let newPath = `/profile/${profileUserId}`;
|
||||
if (page !== 'profile') {
|
||||
newPath += `#${page}`;
|
||||
}
|
||||
window.history.replaceState(null, null, newPath);
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('user'),
|
||||
subSection: this.$t(this.startingPage),
|
||||
subSection: this.$t(page),
|
||||
});
|
||||
},
|
||||
getNextIncentive () {
|
||||
|
||||
@@ -3,6 +3,218 @@
|
||||
id="stats"
|
||||
class="standard-page"
|
||||
>
|
||||
<div
|
||||
id="attributes"
|
||||
class="row"
|
||||
>
|
||||
<hr class="col-12">
|
||||
<h2 class="col-12">
|
||||
{{ $t('attributes') }}
|
||||
</h2>
|
||||
<div
|
||||
v-for="(statInfo, stat) in stats"
|
||||
:key="stat"
|
||||
class="col-12 col-md-6"
|
||||
>
|
||||
<div class="row col-12 stats-column">
|
||||
<div class="col-12 col-md-6 attribute-label">
|
||||
<span
|
||||
class="hint"
|
||||
:popover-title="$t(statInfo.title)"
|
||||
popover-placement="right"
|
||||
:popover="$t(statInfo.popover)"
|
||||
popover-trigger="mouseenter"
|
||||
></span>
|
||||
<div
|
||||
class="stat-title"
|
||||
:class="stat"
|
||||
>
|
||||
{{ $t(statInfo.title) }}
|
||||
</div>
|
||||
<strong class="number">{{ totalStatPoints(stat) | floorWholeNumber }}</strong>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="bonus-stats">
|
||||
<li>
|
||||
<strong>{{ $t('level') }}:</strong>
|
||||
<span>{{ statsComputed.levelBonus[stat] }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('equipment') }}:</strong>
|
||||
<span>{{ statsComputed.gearBonus[stat] }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('class') }}:</strong>
|
||||
<span>{{ statsComputed.classBonus[stat] }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('allocated') }}:</strong>
|
||||
<span>{{ totalAllocatedStats(stat) }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('buffs') }}:</strong>
|
||||
<span>{{ user.stats.buffs[stat] }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showAllocation"
|
||||
class="stat-allocation-info"
|
||||
>
|
||||
<p>{{ $t('statAllocationInfo') }}</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="showAllocation"
|
||||
id="allocation"
|
||||
>
|
||||
<div class="row title-row">
|
||||
<div :class="user.preferences.automaticAllocation ? 'col-12 col-md-6' : 'col-12'">
|
||||
<h3
|
||||
v-if="userLevel100Plus"
|
||||
v-once
|
||||
v-html="$t('noMoreAllocate')"
|
||||
></h3>
|
||||
<div
|
||||
class="points-allocation-header"
|
||||
:class="{'auto-off': !user.preferences.automaticAllocation}"
|
||||
>
|
||||
<h3>
|
||||
{{ $t('pointsAvailable') }}
|
||||
<div
|
||||
v-if="user.stats.points || userLevel100Plus"
|
||||
class="counter badge badge-pill"
|
||||
>
|
||||
{{ pointsRemaining }}
|
||||
</div>
|
||||
</h3>
|
||||
<div class="auto-allocate-toggle">
|
||||
<toggle-switch
|
||||
v-model="user.preferences.automaticAllocation"
|
||||
:label="$t('autoAllocate')"
|
||||
@change="setAutoAllocate()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.preferences.automaticAllocation"
|
||||
class="col-12 col-md-6 allocation-dropdown-container"
|
||||
>
|
||||
<div class="task-allocation-box" @click="toggleAllocationDropdown">
|
||||
<span class="task-allocation-text">{{ allocationModeLabel }}</span>
|
||||
<information-icon
|
||||
tooltip-id="task-allocation-info"
|
||||
:tooltip="allocationModeTooltip"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-chevron"
|
||||
:class="{rotated: showAllocationDropdown}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 14 9"
|
||||
width="14"
|
||||
height="9"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
fill-rule="evenodd"
|
||||
stroke="#A5A1AC"
|
||||
stroke-width="2.5"
|
||||
d="M13 1L7 7 1 1"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showAllocationDropdown" class="allocation-dropdown">
|
||||
<div class="allocation-option">
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.allocationMode"
|
||||
type="radio"
|
||||
name="allocationMode"
|
||||
value="flat"
|
||||
@change="setAllocationMode('flat')"
|
||||
>
|
||||
<span class="option-text">{{ $t('evenAllocation') }}</span>
|
||||
</label>
|
||||
<information-icon
|
||||
tooltip-id="even-allocation-info"
|
||||
:tooltip="$t('evenAllocationPop')"
|
||||
/>
|
||||
</div>
|
||||
<div class="allocation-option">
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.allocationMode"
|
||||
type="radio"
|
||||
name="allocationMode"
|
||||
value="classbased"
|
||||
@change="setAllocationMode('classbased')"
|
||||
>
|
||||
<span class="option-text">{{ $t('classAllocation') }}</span>
|
||||
</label>
|
||||
<information-icon
|
||||
tooltip-id="class-allocation-info"
|
||||
:tooltip="$t('classAllocationPop')"
|
||||
/>
|
||||
</div>
|
||||
<div class="allocation-option">
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.allocationMode"
|
||||
type="radio"
|
||||
name="allocationMode"
|
||||
value="taskbased"
|
||||
@change="setAllocationMode('taskbased')"
|
||||
>
|
||||
<span class="option-text">{{ $t('taskAllocation') }}</span>
|
||||
</label>
|
||||
<information-icon
|
||||
tooltip-id="task-allocation-dropdown-info"
|
||||
:tooltip="$t('taskAllocationPop')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(statInfo, stat) in allocateStatsList"
|
||||
:key="stat"
|
||||
class="col-12 col-md-3"
|
||||
>
|
||||
<div class="box white row col-12">
|
||||
<div class="col-9 text-nowrap">
|
||||
<div :class="stat">
|
||||
{{ $t(stats[stat].title) }}
|
||||
</div>
|
||||
<div class="number">
|
||||
{{ totalAllocatedStats(stat) }}
|
||||
</div>
|
||||
<div class="points">
|
||||
{{ $t('pts') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 arrow-container">
|
||||
<div
|
||||
v-if="user.stats.points > 0"
|
||||
class="triangle-up"
|
||||
@click="allocate(stat)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 12">
|
||||
<path fill="#A5A1AC" d="M10 0l10 12H0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="allocation-divider"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="stats-section-equipment col-12 col-md-6">
|
||||
<h2 class="text-center">
|
||||
@@ -182,144 +394,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="attributes"
|
||||
class="row"
|
||||
>
|
||||
<hr class="col-12">
|
||||
<h2 class="col-12">
|
||||
{{ $t('attributes') }}
|
||||
</h2>
|
||||
<div
|
||||
v-for="(statInfo, stat) in stats"
|
||||
:key="stat"
|
||||
class="col-12 col-md-6"
|
||||
>
|
||||
<div class="row col-12 stats-column">
|
||||
<div class="col-12 col-md-4 attribute-label">
|
||||
<span
|
||||
class="hint"
|
||||
:popover-title="$t(statInfo.title)"
|
||||
popover-placement="right"
|
||||
:popover="$t(statInfo.popover)"
|
||||
popover-trigger="mouseenter"
|
||||
></span>
|
||||
<div
|
||||
class="stat-title"
|
||||
:class="stat"
|
||||
>
|
||||
{{ $t(statInfo.title) }}
|
||||
</div>
|
||||
<strong class="number">{{ totalStatPoints(stat) | floorWholeNumber }}</strong>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="bonus-stats">
|
||||
<li>
|
||||
<strong>{{ $t('level') }}:</strong>
|
||||
{{ statsComputed.levelBonus[stat] }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('equipment') }}:</strong>
|
||||
{{ statsComputed.gearBonus[stat] }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('class') }}:</strong>
|
||||
{{ statsComputed.classBonus[stat] }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('allocated') }}:</strong>
|
||||
{{ totalAllocatedStats(stat) }}
|
||||
</li>
|
||||
<li>
|
||||
<strong>{{ $t('buffs') }}:</strong>
|
||||
{{ user.stats.buffs[stat] }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showAllocation"
|
||||
id="allocation"
|
||||
>
|
||||
<div class="row title-row">
|
||||
<div class="col-12 col-md-6">
|
||||
<h3
|
||||
v-if="userLevel100Plus"
|
||||
v-once
|
||||
v-html="$t('noMoreAllocate')"
|
||||
></h3>
|
||||
<h3>
|
||||
{{ $t('statPoints') }}
|
||||
<div
|
||||
v-if="user.stats.points || userLevel100Plus"
|
||||
class="counter badge badge-pill"
|
||||
>
|
||||
{{ pointsRemaining }}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="float-right">
|
||||
<toggle-switch
|
||||
v-model="user.preferences.automaticAllocation"
|
||||
:label="$t('autoAllocation')"
|
||||
@change="setAutoAllocate()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(statInfo, stat) in allocateStatsList"
|
||||
:key="stat"
|
||||
class="col-12 col-md-3"
|
||||
>
|
||||
<div class="box white row col-12">
|
||||
<div class="col-9 text-nowrap">
|
||||
<div :class="stat">
|
||||
{{ $t(stats[stat].title) }}
|
||||
</div>
|
||||
<div class="number">
|
||||
{{ totalAllocatedStats(stat) }}
|
||||
</div>
|
||||
<div class="points">
|
||||
{{ $t('pts') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div>
|
||||
<div
|
||||
v-if="showStatsSave"
|
||||
class="up"
|
||||
@click="allocate(stat)"
|
||||
></div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
v-if="showStatsSave"
|
||||
class="down"
|
||||
@click="deallocate(stat)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="showStatsSave"
|
||||
class="row save-row"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
@click="saveAttributes()"
|
||||
>
|
||||
{{ loading ? $t('loading') : $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -331,12 +405,12 @@ import keys from 'lodash/keys';
|
||||
import Content from '@/../../common/script/content';
|
||||
import { beastMasterProgress, mountMasterProgress } from '@/../../common/script/count';
|
||||
import autoAllocate from '@/../../common/script/fns/autoAllocate';
|
||||
import allocateBulk from '@/../../common/script/ops/stats/allocateBulk';
|
||||
import statsComputed from '@/../../common/script/libs/statsComputed';
|
||||
import { mapState } from '@/libs/store';
|
||||
import attributesGrid from '@/components/inventory/equipment/attributesGrid';
|
||||
import toggleSwitch from '@/components/ui/toggleSwitch';
|
||||
import Sprite from '@/components/ui/sprite';
|
||||
import InformationIcon from '@/components/ui/informationIcon';
|
||||
|
||||
const DROP_ANIMALS = keys(Content.pets);
|
||||
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;
|
||||
@@ -345,11 +419,12 @@ export default {
|
||||
toggleSwitch,
|
||||
attributesGrid,
|
||||
Sprite,
|
||||
InformationIcon,
|
||||
},
|
||||
props: ['user', 'showAllocation'],
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
showAllocationDropdown: false,
|
||||
equipTypes: {
|
||||
eyewear: this.$t('eyewear'),
|
||||
head: this.$t('headgearCapitalized'),
|
||||
@@ -387,12 +462,6 @@ export default {
|
||||
popover: 'perText',
|
||||
},
|
||||
},
|
||||
statUpdates: {
|
||||
str: 0,
|
||||
int: 0,
|
||||
con: 0,
|
||||
per: 0,
|
||||
},
|
||||
content: Content,
|
||||
};
|
||||
},
|
||||
@@ -412,15 +481,22 @@ export default {
|
||||
userLevel100Plus () {
|
||||
return this.user.stats.lvl >= 100;
|
||||
},
|
||||
showStatsSave () {
|
||||
return Boolean(this.user.stats.points);
|
||||
},
|
||||
pointsRemaining () {
|
||||
let { points } = this.user.stats;
|
||||
Object.values(this.statUpdates).forEach(value => {
|
||||
points -= value;
|
||||
});
|
||||
return points;
|
||||
return this.user.stats.points;
|
||||
},
|
||||
allocationModeLabel () {
|
||||
const mode = this.user.preferences.allocationMode || 'flat';
|
||||
if (mode === 'flat') return this.$t('evenAllocation');
|
||||
if (mode === 'classbased') return this.$t('classAllocation');
|
||||
if (mode === 'taskbased') return this.$t('taskAllocation');
|
||||
return this.$t('evenAllocation');
|
||||
},
|
||||
allocationModeTooltip () {
|
||||
const mode = this.user.preferences.allocationMode || 'flat';
|
||||
if (mode === 'flat') return this.$t('evenAllocationPop');
|
||||
if (mode === 'classbased') return this.$t('classAllocationPop');
|
||||
if (mode === 'taskbased') return this.$t('taskAllocationPop');
|
||||
return this.$t('evenAllocationPop');
|
||||
},
|
||||
|
||||
},
|
||||
@@ -432,10 +508,10 @@ export default {
|
||||
return this.flatGear[key].text();
|
||||
},
|
||||
totalAllocatedStats (stat) {
|
||||
return this.user.stats[stat] + this.statUpdates[stat];
|
||||
return this.user.stats[stat];
|
||||
},
|
||||
totalStatPoints (stat) {
|
||||
return this.statsComputed[stat] + this.statUpdates[stat];
|
||||
return this.statsComputed[stat];
|
||||
},
|
||||
totalCount (objectToCount) {
|
||||
const total = size(objectToCount);
|
||||
@@ -481,37 +557,17 @@ export default {
|
||||
const display = `${stat}/${totalStat}`;
|
||||
return display;
|
||||
},
|
||||
allocate (stat) {
|
||||
if (this.pointsRemaining === 0) return;
|
||||
this.statUpdates[stat] += 1;
|
||||
},
|
||||
deallocate (stat) {
|
||||
if (this.statUpdates[stat] === 0) return;
|
||||
this.statUpdates[stat] -= 1;
|
||||
},
|
||||
async saveAttributes () {
|
||||
this.loading = true;
|
||||
async allocate (stat) {
|
||||
if (this.user.stats.points === 0) return;
|
||||
|
||||
const statUpdates = {};
|
||||
['str', 'int', 'per', 'con'].forEach(stat => {
|
||||
if (this.statUpdates[stat] > 0) statUpdates[stat] = this.statUpdates[stat];
|
||||
});
|
||||
|
||||
// reset statUpdates to zero before request to avoid display errors while waiting for server
|
||||
this.statUpdates = {
|
||||
str: 0,
|
||||
int: 0,
|
||||
con: 0,
|
||||
per: 0,
|
||||
};
|
||||
|
||||
allocateBulk(this.user, { body: { stats: statUpdates } });
|
||||
|
||||
await axios.post('/api/v4/user/allocate-bulk', {
|
||||
stats: statUpdates,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
try {
|
||||
const response = await axios.post(`/api/v4/user/allocate?stat=${stat}`);
|
||||
if (response.data && response.data.data) {
|
||||
this.$store.state.user.data.stats = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error allocating stat point:', error);
|
||||
}
|
||||
},
|
||||
allocateNow () {
|
||||
autoAllocate(this.user);
|
||||
@@ -519,11 +575,21 @@ export default {
|
||||
setAutoAllocate () {
|
||||
const settings = {
|
||||
'preferences.automaticAllocation': Boolean(this.user.preferences.automaticAllocation),
|
||||
'preferences.allocationMode': 'taskbased',
|
||||
'preferences.allocationMode': this.user.preferences.allocationMode || 'flat',
|
||||
};
|
||||
|
||||
this.$store.dispatch('user:set', settings);
|
||||
},
|
||||
toggleAllocationDropdown () {
|
||||
this.showAllocationDropdown = !this.showAllocationDropdown;
|
||||
},
|
||||
setAllocationMode (mode) {
|
||||
const settings = {
|
||||
'preferences.allocationMode': mode,
|
||||
};
|
||||
this.$store.dispatch('user:set', settings);
|
||||
this.showAllocationDropdown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -555,6 +621,15 @@ export default {
|
||||
|
||||
.stat-title {
|
||||
text-transform: uppercase;
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
margin-top: 26px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px dashed;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.str {
|
||||
@@ -577,6 +652,80 @@ export default {
|
||||
.title-row {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
h3 {
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-10;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.points-allocation-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1em;
|
||||
height: 40px;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
&.auto-off {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.auto-allocate-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease-in-out;
|
||||
|
||||
::v-deep .toggle-switch-outer {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
::v-deep .toggle-switch-description {
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-10;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
::v-deep .toggle-switch-label {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
::v-deep .toggle-switch-switch {
|
||||
margin: auto;
|
||||
margin-left: -2px;
|
||||
margin-right: -2px;
|
||||
}
|
||||
|
||||
::v-deep .toggle-switch-inner:before {
|
||||
background-color: #9A62FF;
|
||||
}
|
||||
}
|
||||
|
||||
.allocation-dropdown-container {
|
||||
animation: slideInFromRight 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.counter.badge {
|
||||
@@ -584,10 +733,108 @@ export default {
|
||||
top: -0.25em;
|
||||
left: 0.5em;
|
||||
color: #fff;
|
||||
background-color: #ff944c;
|
||||
background-color: $orange-50;
|
||||
box-shadow: 0 1px 1px 0 rgba(26, 24, 29, 0.12);
|
||||
}
|
||||
|
||||
.task-allocation-box {
|
||||
height: 40px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.15), 0 1px 4px 0 rgba(26, 24, 29, 0.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-allocation-text {
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-50;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-chevron {
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.allocation-dropdown {
|
||||
position: absolute;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px 0 rgba(26, 24, 29, 0.2);
|
||||
margin-top: 4px;
|
||||
padding: 8px 0;
|
||||
z-index: 10;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.allocation-option {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
.allocation-divider {
|
||||
height: 1px;
|
||||
background-color: $gray-500;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.str, .int, .con, .per {
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
text-transform: uppercase;
|
||||
margin-top: 26px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px dashed;
|
||||
display: block;
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 148px;
|
||||
height: 84px;
|
||||
@@ -599,41 +846,57 @@ export default {
|
||||
}
|
||||
|
||||
.number {
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 40px;
|
||||
line-height: 48px;
|
||||
letter-spacing: 0px;
|
||||
text-align: left;
|
||||
color: #686274;
|
||||
color: $gray-100;
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.points {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
line-height: 1.67;
|
||||
font-family: Roboto;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
text-align: left;
|
||||
color: #878190;
|
||||
margin-left: .5em;
|
||||
color: $gray-200;
|
||||
margin-left: 0.5em;
|
||||
vertical-align: baseline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.up, .down {
|
||||
border: solid #a5a1ac;
|
||||
border-width: 0 3px 3px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
.arrow-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.up:hover, .down:hover {
|
||||
.triangle-up {
|
||||
width: 21px !important;
|
||||
height: 13px !important;
|
||||
min-width: 21px !important;
|
||||
min-height: 13px !important;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 21px !important;
|
||||
height: 13px !important;
|
||||
min-width: 21px !important;
|
||||
min-height: 13px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.up {
|
||||
transform: rotate(-135deg);
|
||||
-webkit-transform: rotate(-135deg);
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.down {
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
.triangle-up:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,11 +906,60 @@ export default {
|
||||
font-size: 64px;
|
||||
font-weight: bold;
|
||||
color: #686274;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.attribute-label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.bonus-stats {
|
||||
border-left: 1px solid $gray-500;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 1em;
|
||||
padding-top: 10px;
|
||||
|
||||
li {
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-300;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-right: 1em;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-allocation-info {
|
||||
margin-top: 3em;
|
||||
margin-bottom: 0.5em;
|
||||
padding-top: 2em;
|
||||
padding-bottom: 0.25em;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
font-family: Roboto;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0px;
|
||||
color: $gray-100;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.well {
|
||||
@@ -759,7 +1071,7 @@ export default {
|
||||
margin: 0;
|
||||
}
|
||||
.col-9 div:first-child {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,26 +2,24 @@ 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 AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
|
||||
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
|
||||
|
||||
const REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction'];
|
||||
const ALLOWED_HIT_TYPES = [
|
||||
'pageview',
|
||||
'screenview',
|
||||
'event',
|
||||
'transaction',
|
||||
'item',
|
||||
'social',
|
||||
'exception',
|
||||
'timing',
|
||||
];
|
||||
let analyticsLoading = false;
|
||||
let analyticsReady = false;
|
||||
|
||||
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 +32,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,24 +62,26 @@ 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 safeSetup (userId) {
|
||||
if (analyticsLoading || analyticsReady) return;
|
||||
analyticsLoading = true;
|
||||
amplitude.getInstance().init(AMPLITUDE_KEY, userId);
|
||||
analyticsReady = true;
|
||||
analyticsLoading = false;
|
||||
}
|
||||
|
||||
export function track (properties, options = {}) {
|
||||
const user = _getConsentedUser();
|
||||
if (!user) return;
|
||||
safeSetup(user._id);
|
||||
// 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);
|
||||
@@ -101,45 +90,15 @@ export function track (properties, options = {}) {
|
||||
}
|
||||
|
||||
export function updateUser (properties = {}) {
|
||||
const user = _getConsentedUser();
|
||||
if (!user) return;
|
||||
safeSetup(user._id);
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import habiticaMarkdown from 'habitica-markdown/withMentions';
|
||||
import escapeRegExp from 'lodash/escapeRegExp';
|
||||
|
||||
export default function renderWithMentions (text, user) {
|
||||
if (!text) return null;
|
||||
const env = { userName: user.auth.local.username, displayName: user.profile.name };
|
||||
return habiticaMarkdown.render(String(text), env);
|
||||
const env = { userName: user.auth.local.username };
|
||||
let html = habiticaMarkdown.render(String(text), env);
|
||||
|
||||
if (user.auth.local.username) {
|
||||
const username = escapeRegExp(user.auth.local.username);
|
||||
const regex = new RegExp(`(<span class="at-text">@)(${username})(</span>)`, 'gi');
|
||||
html = html.replace(regex, (match, p1, p2, p3) => `${p1.replace('at-text', 'at-text at-highlight')}${p2}${p3}`);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ export default [
|
||||
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
|
||||
},
|
||||
{
|
||||
name: 'heyeilatan',
|
||||
name: 'fizself',
|
||||
type: 'Staff',
|
||||
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
|
||||
uuid: 'e39ea3eb-28d2-48da-8568-7a5b0e64498e',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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
@@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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,202 @@
|
||||
<template>
|
||||
<tr>
|
||||
<td colspan="3"
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
<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 colspan="3"
|
||||
v-if="mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
<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
|
||||
class="mb-auto"
|
||||
v-model="user.preferences.analyticsConsent"
|
||||
@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,11 +119,12 @@ 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';
|
||||
@@ -159,6 +161,7 @@ export default {
|
||||
GemsPromoBanner,
|
||||
GiftPromoBanner,
|
||||
BirthdayBanner,
|
||||
PrivacyBanner,
|
||||
notificationsDisplay,
|
||||
BuyModal,
|
||||
SelectMembersModal,
|
||||
@@ -260,18 +263,24 @@ export default {
|
||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
Analytics.setUser();
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Analytics.updateUser();
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (window && window['habitica-i18n']) {
|
||||
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return axios.get(
|
||||
'/api/v4/i18n/browser-script',
|
||||
{
|
||||
|
||||