Compare commits

..

28 Commits

Author SHA1 Message Date
Hafiz
17caa2159d missed add new line 2025-08-19 15:00:51 -05:00
Hafiz
36520ae6a5 lint 2025-08-19 14:55:59 -05:00
Hafiz
315a942c69 remove redundant disabled styles in task modals
The .disabled class conflicting with existing disabled state implementations
2025-08-19 14:48:26 -05:00
Hafiz
9a1fb18959 Merge remote-tracking branch 'origin/develop' into qa/bat 2025-08-12 09:46:07 -05:00
Phillip Thelen
2ea0b64603 improve blocker form display 2025-08-05 14:57:25 +02:00
Phillip Thelen
bd1aa1e417 validate blocker value during input 2025-08-05 14:45:23 +02:00
Phillip Thelen
7c49b845d6 add option to errorHandler to skip logging 2025-08-04 17:40:26 +02:00
Phillip Thelen
1ee172139d lint fix 2025-08-04 16:32:40 +02:00
Phillip Thelen
6447b9ab4b update block error strings 2025-08-04 16:03:55 +02:00
Phillip Thelen
5c414099d9 improve navbar display for non fullAccess admin 2025-08-04 14:46:05 +02:00
Phillip Thelen
5e8e1179aa fix managing permissions from admin 2025-08-04 14:45:47 +02:00
Phillip Thelen
7e86a62624 improve permission check 2025-08-04 14:33:09 +02:00
Phillip Thelen
1ba9dda0ed add new permission for managing blockers 2025-08-04 14:21:36 +02:00
Phillip Thelen
227e5ceaa8 fix import 2025-07-30 11:26:55 +02:00
Phillip Thelen
f77ab5a3ab lint fixes 2025-07-30 11:26:55 +02:00
Phillip Thelen
1916faf647 fix 2025-07-30 11:26:55 +02:00
Phillip Thelen
80ecb5cef1 lint fix 2025-07-30 11:26:55 +02:00
Phillip Thelen
75c36e6622 add blocker to block emails from registration 2025-07-30 11:26:55 +02:00
Phillip Thelen
78330c975a Improve blocker UI 2025-07-30 11:26:55 +02:00
Phillip Thelen
95266f6cb3 improve test coverage 2025-07-30 11:26:55 +02:00
Phillip Thelen
e9b2c1b51a restructure admin pages 2025-07-30 11:26:54 +02:00
Phillip Thelen
2a2bea07ab Add UI for managing blockers 2025-07-30 11:26:54 +02:00
Phillip Thelen
ea60ddbf4c Tweak wording 2025-07-30 11:25:51 +02:00
Phillip Thelen
1c2ca0e478 correctly reset local data after creating blocker 2025-07-30 11:25:51 +02:00
Phillip Thelen
ef2b7eb928 Add UI for managing blockers 2025-07-30 11:25:51 +02:00
Phillip Thelen
3d16387a61 add new frontend files 2025-07-30 11:25:41 +02:00
Phillip Thelen
93b7770eaa begin building general blocking solution 2025-07-30 11:25:41 +02:00
Phillip Thelen
a9f84d3307 Read IP blocks from database 2025-07-30 11:25:41 +02:00
376 changed files with 3448 additions and 8064 deletions

View File

@@ -8,26 +8,18 @@
"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",
"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",
"EMAIL_SERVER_AUTH_PASSWORD": "password",
"EMAIL_SERVER_AUTH_USER": "user",
"EMAIL_SERVER_URL": "http://example.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",
@@ -37,16 +29,15 @@
"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",
@@ -64,34 +55,44 @@
"PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
"PORT": 3000,
"PUSH_CONFIGS_APN_ENABLED": "false",
"PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
"PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
"PUSH_CONFIGS_APN_KEY_ID": "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",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"TIME_TRAVEL_ENABLED": "false",
"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",
"WEB_CONCURRENCY": 1
"TIME_TRAVEL_ENABLED": "false",
"DEBUG_ENABLED": "false",
"CONTENT_SWITCHOVER_TIME_OFFSET": 8,
"SLOW_REQUEST_THRESHOLD": 1000
}

View File

@@ -10,7 +10,7 @@ function setUpServer () {
setupNconf();
// We require src/server and not src/index because
// We require src/server and npt src/index because
// 1. nconf is already setup
// 2. we don't need clustering
require('../website/server/server'); // eslint-disable-line global-require

View File

@@ -11,7 +11,7 @@ const progressCount = 1000;
let count = 0;
/*
* Award every extant piece of equippable gear
* Award users every extant pet and mount
*/
async function updateUser (user) {

View File

@@ -3,8 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = 'YYYYMMDD_take_this';
const CHALLENGE_ID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
const MIGRATION_NAME = '20181203_take_this';
const progressCount = 1000;
let count = 0;
@@ -42,15 +41,15 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return User.updateOne({ _id: user._id }, { $set: set, $push: push }).exec();
return User.update({ _id: user._id }, { $set: set, $push: push }).exec();
}
return User.updateOne({ _id: user._id }, { $set: set }).exec();
return User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
challenges: CHALLENGE_ID,
challenges: '00708425-d477-41a5-bf27-6270466e7976',
};
const fields = {

144
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.41.3",
"version": "5.38.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.41.3",
"version": "5.38.2",
"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.8.1",
"cookie-session": "^2.1.1",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"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.1",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"on-headers": "^1.1.0",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
@@ -75,6 +75,7 @@
"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",
@@ -6086,10 +6087,9 @@
}
},
"node_modules/brace-expansion": {
"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",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6906,23 +6906,30 @@
}
},
"node_modules/compression": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"license": "MIT",
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
"integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"accepts": "~1.3.5",
"bytes": "3.0.0",
"compressible": "~2.0.16",
"debug": "2.6.9",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
"on-headers": "~1.0.2",
"safe-buffer": "5.1.2",
"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",
@@ -6936,35 +6943,6 @@
"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",
@@ -7135,14 +7113,13 @@
}
},
"node_modules/cookie-session": {
"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",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-2.0.0.tgz",
"integrity": "sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg==",
"dependencies": {
"cookies": "0.9.1",
"cookies": "0.8.0",
"debug": "3.2.7",
"on-headers": "~1.1.0",
"on-headers": "~1.0.2",
"safe-buffer": "5.2.1"
},
"engines": {
@@ -7187,10 +7164,9 @@
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
},
"node_modules/cookies": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz",
"integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==",
"license": "MIT",
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz",
"integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==",
"dependencies": {
"depd": "~2.0.0",
"keygrip": "~1.1.0"
@@ -11449,10 +11425,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -14030,7 +14005,6 @@
"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"
},
@@ -15545,16 +15519,15 @@
"dev": true
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
"license": "MIT",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
"integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==",
"dependencies": {
"basic-auth": "~2.0.1",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-finished": "~2.3.0",
"on-headers": "~1.1.0"
"on-headers": "~1.0.2"
},
"engines": {
"node": ">= 0.8.0"
@@ -16883,10 +16856,9 @@
}
},
"node_modules/on-headers": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
"license": "MIT",
"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==",
"engines": {
"node": ">= 0.8"
}
@@ -20529,10 +20501,9 @@
}
},
"node_modules/tar-fs": {
"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",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
"integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
@@ -21072,7 +21043,6 @@
"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"
}
@@ -21398,6 +21368,26 @@
"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",

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.41.3",
"version": "5.38.2",
"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.8.1",
"cookie-session": "^2.1.1",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"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.1",
"morgan": "^1.10.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"on-headers": "^1.1.0",
"on-headers": "^1.0.2",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
@@ -70,6 +70,7 @@
"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",
@@ -105,8 +106,8 @@
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "node --watch --inspect ./website/server/index.js",
"mongo:dev": "run-rs -v 7.0.23 -l 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",
"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",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"

View File

@@ -1,11 +1,15 @@
/* 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(() => {
@@ -33,6 +37,8 @@ describe('analyticsService', () => {
data;
beforeEach(() => {
Visitor.prototype.event.yields();
eventType = 'Cron';
data = {
category: 'behavior',
@@ -43,11 +49,6 @@ describe('analyticsService', () => {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
@@ -294,9 +295,6 @@ describe('analyticsService', () => {
rewards: [{ _id: 'reward' }],
balance: 12,
loginIncentives: 1,
preferences: {
analyticsConsent: true,
},
};
data.user = user;
@@ -328,12 +326,37 @@ 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 data; let
itemSpy;
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',
@@ -347,11 +370,6 @@ describe('analyticsService', () => {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
@@ -515,9 +533,6 @@ describe('analyticsService', () => {
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
preferences: {
analyticsConsent: true,
},
};
data.user = user;
@@ -546,6 +561,26 @@ 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', () => {

View File

@@ -34,7 +34,6 @@ describe('bug-report', () => {
emailData: {
BROWSER_UA: userAgent,
REPORT_MSG: userMessage,
USER_ANALYTICS: undefined,
USER_CLASS: 'warrior',
USER_CONSECUTIVE_MONTHS: 0,
USER_COSTUME: 'false',

View File

@@ -150,7 +150,7 @@ describe('emails', () => {
sendTxn(mailingInfo, emailType);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
expect(got.post).to.be.calledWith('undefined/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('http://example.com/job', sinon.match({
expect(got.post).to.be.calledWith('undefined/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('http://example.com/job', sinon.match({
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
json: {
data: {
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),

View File

@@ -47,12 +47,6 @@ 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);

View File

@@ -12,33 +12,11 @@ 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';
@@ -47,9 +25,13 @@ 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);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isCanceled').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
productId: 'com.habitrpg.ios.Habitica.21gems',
@@ -60,6 +42,12 @@ 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();
});
@@ -221,6 +209,9 @@ 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];
@@ -232,10 +223,12 @@ 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(),
@@ -257,6 +250,10 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
@@ -273,29 +270,6 @@ 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',
@@ -600,7 +574,8 @@ describe('Apple Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let headers; let customerId; let
expirationDate;
let paymentCancelSubscriptionSpy;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
beforeEach(async () => {
token = 'test-token';
@@ -609,7 +584,8 @@ describe('Apple Payments', () => {
customerId = 'test-customerId';
expirationDate = moment.utc();
iapValidateStub.restore();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({
expirationDate,
@@ -617,8 +593,8 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -630,7 +606,13 @@ describe('Apple Payments', () => {
});
afterEach(() => {
paymentCancelSubscriptionSpy.restore();
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
it('should throw an error if we are missing a subscription', async () => {
@@ -713,8 +695,6 @@ 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;

View File

@@ -11,36 +11,12 @@ 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';
@@ -49,7 +25,21 @@ 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 () => {
@@ -170,7 +160,8 @@ describe('Google Payments', () => {
describe('subscribe', () => {
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
nextPaymentProcessing;
let paymentsCreateSubscritionStub;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentsCreateSubscritionStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
@@ -182,12 +173,19 @@ 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();
});
@@ -245,7 +243,7 @@ describe('Google Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let signature; let headers; let customerId; let
expirationDate;
let iapGetPurchaseDataStub; let
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
beforeEach(async () => {
@@ -255,12 +253,17 @@ 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';
@@ -273,6 +276,9 @@ describe('Google Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
@@ -302,8 +308,6 @@ 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;
@@ -328,20 +332,11 @@ 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([{
startTimeMillis: expirationDate.valueOf(),
expirationDate,
autoRenewing: false,
}, {
startTimeMillis: laterDate.valueOf(),
expirationDate: laterDate,
autoRenewing: false,
},
.returns([{ expirationDate, autoRenewing: false },
{ expirationDate: laterDate, autoRenewing: false },
]);
await googlePayments.cancelSubscribe(user, headers);
@@ -370,12 +365,7 @@ describe('Google Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ autoRenewing: true }]);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -398,12 +388,8 @@ describe('Google Payments', () => {
.returns([{ expirationDate, autoRenewing: false },
{ autoRenewing: true },
{ expirationDate, autoRenewing: false }]);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {

View File

@@ -117,6 +117,15 @@ describe('Blocker middleware', () => {
checkIPBlockedErrorThrown(next);
});
it('throws when the ip is blocked', () => {
req.ip = '192.168.1.1';
sandbox.stub(nconf, 'get').withArgs('BLOCKED_IPS').returns('192.168.1.1');
const attachBlocker = requireAgain(pathToBlocker).default;
attachBlocker(req, res, next);
checkIPBlockedErrorThrown(next);
});
});
describe('Blocking clients', () => {

View File

@@ -238,18 +238,6 @@ 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.

View File

@@ -1,7 +1,6 @@
import {
requester,
translate as t,
generateUser,
} from '../../../../helpers/api-integration/v3';
import i18n from '../../../../../website/common/script/i18n';
@@ -57,28 +56,4 @@ 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'));
});
});
});

View File

@@ -25,7 +25,7 @@ describe('GET /user/auth/apple', () => {
});
it('registers a new user', async () => {
const response = await api.get(`${appleEndpoint}?allowRegister=true`);
const response = await api.get(appleEndpoint);
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}?allowRegister=true`);
const registerResponse = await api.get(appleEndpoint);
const response = await api.get(appleEndpoint);

View File

@@ -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, username: user.auth.local.username }),
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
});
});
@@ -110,18 +110,6 @@ describe('POST /user/auth/local/login', () => {
expect(isValidPassword).to.equal(true);
});
it('sets auth.timestamps.updated', async () => {
const oldUpdated = new Date(user.auth.timestamps.updated);
// login
await api.post(endpoint, {
username: user.auth.local.email,
password,
});
await user.sync();
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
});
it('user uses social authentication and has no password', async () => {
await user.unset({
'auth.local.hashed_password': 1,

View File

@@ -6,7 +6,6 @@ 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;
@@ -65,18 +64,6 @@ 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
@@ -144,36 +131,6 @@ 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
@@ -210,24 +167,5 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
it('sets auth.timestamps.updated', async () => {
let oldUpdated = new Date(user.auth.timestamps.updated);
await user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
await user.sync();
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
oldUpdated = new Date(user.auth.timestamps.updated);
// Do it again to ensure it updates even when nothing else changes
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
await user.sync();
expect(user.auth.timestamps.updated).to.be.greaterThan(oldUpdated);
});
});
});

View File

@@ -1,66 +0,0 @@
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;
});
});

View File

@@ -37,7 +37,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.3.6",
"vite": "^6.0.0",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
@@ -3393,10 +3393,9 @@
"integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA=="
},
"node_modules/brace-expansion": {
"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",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -4025,15 +4024,13 @@
}
},
"node_modules/es-set-tostringtag": {
"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",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
"integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
"get-intrinsic": "^1.2.2",
"has-tostringtag": "^1.0.0",
"hasown": "^2.0.0"
},
"engines": {
"node": ">= 0.4"
@@ -5084,15 +5081,12 @@
}
},
"node_modules/form-data": {
"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",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"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": {
@@ -5122,20 +5116,6 @@
"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",
@@ -5384,12 +5364,11 @@
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
"integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
"dependencies": {
"has-symbols": "^1.0.3"
"has-symbols": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
@@ -7134,21 +7113,6 @@
"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",
@@ -8551,10 +8515,9 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

View File

@@ -41,7 +41,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.3.6",
"vite": "^6.0.0",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",

View File

@@ -108,6 +108,7 @@
<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';
@@ -149,6 +150,10 @@ 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
@@ -203,32 +208,17 @@ 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 deleted their account,
// Don't show errors from getting user details. These users have delete their account,
// but their chat message still exists.
const configExists = Boolean(error.response) && Boolean(error.response.config);
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);
}
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);
}
const errorData = error.response.data;

View File

@@ -635,11 +635,6 @@
width: 141px;
height: 147px;
}
.background_autumn_swamp {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_swamp.png');
width: 141px;
height: 147px;
}
.background_autumn_tree_tunnel {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_tree_tunnel.png');
width: 141px;
@@ -815,11 +810,6 @@
width: 141px;
height: 147px;
}
.background_castle_keep_with_banners {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_castle_keep_with_banners.png');
width: 141px;
height: 147px;
}
.background_cemetery_gate {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_cemetery_gate.png');
width: 141px;
@@ -1556,11 +1546,6 @@
width: 141px;
height: 147px;
}
.background_inside_forest_witchs_cottage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_inside_forest_witchs_cottage.png');
width: 141px;
height: 147px;
}
.background_iridescent_clouds {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_iridescent_clouds.png');
width: 141px;
@@ -29580,16 +29565,6 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_blackPartyDress {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blackPartyDress.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_blacksmithsApron {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blacksmithsApron.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_blueMoonShozoku {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_blueMoonShozoku.png');
width: 114px;
@@ -30025,11 +30000,6 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_softOrangeSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softOrangeSuit.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_softPinkSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softPinkSuit.png');
width: 114px;
@@ -30230,21 +30200,11 @@
width: 114px;
height: 90px;
}
.head_armoire_blackHairbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackHairbow.png');
width: 90px;
height: 90px;
}
.head_armoire_blackSpookySorceryHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blackSpookySorceryHat.png');
width: 114px;
height: 90px;
}
.head_armoire_blacksmithsGoggles {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blacksmithsGoggles.png');
width: 114px;
height: 90px;
}
.head_armoire_blueFloppyHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_blueFloppyHat.png');
width: 90px;
@@ -30355,11 +30315,6 @@
width: 114px;
height: 90px;
}
.head_armoire_floppyOrangeHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyOrangeHat.png');
width: 114px;
height: 90px;
}
.head_armoire_flutteryWig {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
width: 114px;
@@ -31055,11 +31010,6 @@
width: 114px;
height: 90px;
}
.shield_armoire_softOrangePillow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softOrangePillow.png');
width: 114px;
height: 90px;
}
.shield_armoire_softPinkPillow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softPinkPillow.png');
width: 114px;
@@ -31200,16 +31150,6 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_blackPartyDress {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blackPartyDress.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_blacksmithsApron {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blacksmithsApron.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_blueMoonShozoku {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_blueMoonShozoku.png');
width: 114px;
@@ -31645,11 +31585,6 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_softOrangeSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softOrangeSuit.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_softPinkSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softPinkSuit.png');
width: 114px;
@@ -31800,11 +31735,6 @@
width: 114px;
height: 90px;
}
.weapon_armoire_blacksmithsHammer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blacksmithsHammer.png');
width: 114px;
height: 90px;
}
.weapon_armoire_blueKite {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_blueKite.png');
width: 114px;
@@ -33095,26 +33025,6 @@
width: 114px;
height: 90px;
}
.broad_armor_special_fall2025Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2025Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Mage.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2025Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Rogue.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2025Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2025Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
width: 90px;
@@ -33345,26 +33255,6 @@
width: 114px;
height: 90px;
}
.head_special_fall2025Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Healer.png');
width: 114px;
height: 90px;
}
.head_special_fall2025Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Mage.png');
width: 114px;
height: 90px;
}
.head_special_fall2025Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Rogue.png');
width: 114px;
height: 90px;
}
.head_special_fall2025Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2025Warrior.png');
width: 114px;
height: 90px;
}
.head_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
width: 90px;
@@ -33535,21 +33425,6 @@
width: 114px;
height: 90px;
}
.shield_special_fall2025Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Healer.png');
width: 114px;
height: 90px;
}
.shield_special_fall2025Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Rogue.png');
width: 114px;
height: 90px;
}
.shield_special_fall2025Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2025Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
width: 90px;
@@ -33765,26 +33640,6 @@
width: 114px;
height: 90px;
}
.slim_armor_special_fall2025Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2025Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Mage.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2025Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Rogue.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2025Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2025Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
width: 90px;
@@ -34005,26 +33860,6 @@
width: 114px;
height: 90px;
}
.weapon_special_fall2025Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2025Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Mage.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2025Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Rogue.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2025Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2025Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
width: 90px;
@@ -35830,41 +35665,6 @@
width: 117px;
height: 120px;
}
.back_mystery_202510 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202510.png');
width: 114px;
height: 90px;
}
.body_mystery_202509 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_mystery_202509.png');
width: 117px;
height: 120px;
}
.broad_armor_mystery_202509 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202509.png');
width: 117px;
height: 120px;
}
.eyewear_mystery_202510 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202510.png');
width: 114px;
height: 90px;
}
.shield_mystery_202511 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202511.png');
width: 114px;
height: 90px;
}
.slim_armor_mystery_202509 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202509.png');
width: 117px;
height: 120px;
}
.weapon_mystery_202511 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202511.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_301404 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
width: 90px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,9 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 448 B

View File

@@ -1,37 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -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:not(:disabled):not(.disabled), &:focus {
&:hover, &:focus {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&.btn-flat {
@@ -28,25 +28,14 @@
&:disabled, &.disabled {
cursor: default;
color: $gray-200;
opacity: 1;
background-color: transparent;
color: $gray-50;
opacity: 0.75;
box-shadow: none;
background-color: $gray-700;
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-200;
color: $gray-300;
}
}
@@ -175,6 +164,7 @@
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;
@@ -252,32 +242,29 @@
}
.btn-info {
background-color: $blue-100;
color: $black;
font-weight: 700;
background: $blue-50;
border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 2px 0 rgba($black, 0.24);
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
&:disabled {
background-color: $white;
background: $blue-50;
box-shadow: none;
}
&:hover:not(:disabled):not(.disabled) {
&:hover {
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-50;
color: $black;
background-color: $blue-100;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
&:active:not(:disabled):not(.disabled), &.active:not(:disabled):not(.disabled) {

View File

@@ -1,168 +0,0 @@
// 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;
}
}
}
}

View File

@@ -1,16 +0,0 @@
.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%;
}
}

View File

@@ -14,7 +14,7 @@
color: $purple-200;
}
li, p:not(.purple-600) {
li, p {
font-size: 16px;
}

View File

@@ -111,10 +111,6 @@ h4 {
background-color: $green-100 !important;
}
.bg-purple-50 {
background-color: $purple-50 !important;
}
.bg-purple-100 {
background-color: $purple-100 !important;
}
@@ -123,10 +119,6 @@ h4 {
background-color: $purple-300 !important;
}
.bg-yellow-50 {
background-color: $yellow-50 !important;
}
.bg-white {
background-color: $white !important;
}
@@ -139,10 +131,6 @@ h4 {
color: $gray-50 !important;
}
.gray-100 {
color: $gray-100 !important;
}
.gray-200 {
color: $gray-200 !important;
}

View File

@@ -1,8 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 504 B

View File

@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 261 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -1,29 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,29 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,5 +1,5 @@
<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 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>

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -117,7 +117,7 @@ export default {
closeWithAction () {
this.close();
setTimeout(() => {
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
this.$router.push({ name: 'achievements' });
}, 200);
},
},

View File

@@ -81,7 +81,7 @@ export default {
watch: {
userIdentifier () {
this.isSearching = true;
this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.isSearching = false;
if (users.length === 1) {
this.loadUser(users[0]._id);

View File

@@ -5,12 +5,6 @@
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"
@@ -102,53 +96,6 @@
: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>
@@ -237,8 +184,6 @@ export default {
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
deleteHabiticaAccount: true,
deleteAmplitudeData: true,
};
},
watch: {
@@ -304,25 +249,6 @@ 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]) {

View File

@@ -37,11 +37,7 @@
Party ID
</label>
<strong class="col-sm-9 col-form-label">
<router-link
:to="{'name': 'groupAdminGroup', 'params': {'groupId': groupPartyData._id}}"
>
{{ groupPartyData._id }}
</router-link>
{{ groupPartyData._id }}
</strong>
</div>
<div class="form-group row">

View File

@@ -15,25 +15,6 @@
: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"
@@ -65,7 +46,7 @@
class="form-control"
type="text"
>
<option value="Group Plan">
<option value="groupPlan">
Group Plan
</option>
<option value="Stripe">
@@ -173,11 +154,7 @@
>
<div class="card-body">
<h6 class="card-title">
<router-link
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
>
{{ group.name }}
</router-link>
{{ group.name }}
<small class="float-right">{{ group._id }}</small>
</h6>
<p class="card-text">
@@ -268,7 +245,8 @@
</div>
</div>
<small
v-if="isSubscribed() && !isCancelled()"
v-if="!hero.purchased.plan.dateTerminated
&& hero.purchased.plan.planId"
class="text-success"
>
The subscription does not have a termination date and is active.
@@ -441,79 +419,6 @@
>
</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"
@@ -569,36 +474,17 @@
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.form-group {
margin-bottom: 0.4rem;
}
.input-group-append {
width: auto;
.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 {
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
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;
color: $gray-200;
}
}
</style>
<script>
@@ -609,55 +495,6 @@ 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,
@@ -683,7 +520,6 @@ export default {
isConvertingToGroupPlan: false,
groupPlanID: '',
subscriptionBlocks,
paymentDetails: null,
};
},
computed: {
@@ -717,9 +553,6 @@ export default {
}
return terminationDate;
},
playOrdersUrl () {
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
},
},
methods: {
dateFormat (date) {
@@ -750,20 +583,6 @@ 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) {
@@ -782,31 +601,6 @@ 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>

View File

@@ -226,7 +226,7 @@ export default {
}
},
async retrieveUserHistory () {
const history = await this.$store.dispatch('admin:getUserHistory', { userIdentifier: this.hero._id });
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
this.armoire = history.armoire;
this.questInviteResponses = history.questInviteResponses;
this.cron = history.cron;

View File

@@ -8,13 +8,6 @@
>
{{ $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"

View File

@@ -1,47 +0,0 @@
<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>

View File

@@ -1,45 +0,0 @@
<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>

View File

@@ -1,69 +0,0 @@
<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>

View File

@@ -1,29 +0,0 @@
<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>

View File

@@ -1,93 +0,0 @@
<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>

View File

@@ -1,53 +0,0 @@
<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>

View File

@@ -1,7 +1,6 @@
<template>
<div>
<buy-gems-modal v-if="user" />
<privacy-modal />
<footer>
<!-- Product -->
<div class="product">
@@ -849,7 +848,6 @@ 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';
@@ -866,7 +864,6 @@ if (import.meta.env.TIME_TRAVEL_ENABLED === 'true') {
export default {
components: {
buyGemsModal,
privacyModal,
},
mixins: [
reportBug,

View File

@@ -140,6 +140,11 @@
>
{{ $t('passwordConfirmationMatch') }}
</div>
<small
v-once
class="form-text"
v-html="$t('termsAndAgreement')"
></small>
</div>
<div class="text-center">
<div

View File

@@ -3,9 +3,6 @@
<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"
@@ -13,18 +10,17 @@
>
<div class="text-center">
<div>
<a
href="/static/home"
class="svg-icon svg habitica-logo mx-auto mb-4"
<div
class="svg-icon svg habitica-logo"
v-html="icons.habiticaIcon"
></a>
></div>
</div>
</div>
<div class="form-group">
<div>
<div class="form-group row text-center">
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="proceed('google')"
@click="socialAuth('google')"
>
<div
class="svg-icon social-icon"
@@ -33,16 +29,18 @@
<div
class="text"
>
{{ $t('signUpWithSocial', {social: 'Google'}) }}
{{ registering
? $t('signUpWithSocial', {social: 'Google'})
: $t('loginWithSocial', {social: 'Google'}) }}
</div>
</div>
</div>
</div>
<div class="form-group">
<div>
<div class="form-group row text-center">
<div class="col-12 col-md-12">
<div
class="btn btn-secondary social-button"
@click="proceed('apple')"
@click="socialAuth('apple')"
>
<div
class="svg-icon social-icon"
@@ -51,18 +49,43 @@
<div
class="text"
>
{{ $t('signUpWithSocial', {social: 'Apple'}) }}
{{ registering
? $t('signUpWithSocial', {social: 'Apple'})
: $t('loginWithSocial', {social: 'Apple'}) }}
</div>
</div>
</div>
</div>
<div class="strike mb-3">
<div class="strike">
<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
@@ -71,22 +94,11 @@
<input
id="usernameInput"
v-model="username"
class="form-control dark"
class="form-control"
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"
@@ -98,25 +110,13 @@
<input
id="emailInput"
v-model="email"
class="form-control dark"
class="form-control"
type="email"
:placeholder="$t('emailPlaceholder')"
:class="{
'input-invalid input-with-error': emailError,
'input-valid': emailValid,
}"
:class="{'input-invalid': emailInvalid, 'input-valid': emailValid}"
>
<div
v-if="emailError"
class="input-error"
>
{{ emailError }}
</div>
</div>
<div
class="form-group"
:class="{ 'mt-2': usernameIssues.length > 0 }"
>
<div class="form-group">
<label
v-once
for="passwordInput"
@@ -130,12 +130,12 @@
<input
id="passwordInput"
v-model="password"
class="form-control dark"
class="form-control"
type="password"
:placeholder="$t(registering ? 'passwordPlaceholder' : 'password')"
:class="{
'input-invalid input-with-error': passwordInvalid,
'input-valid': passwordValid
'input-invalid input-with-error': registering && passwordInvalid,
'input-valid': registering && passwordValid
}"
>
<div
@@ -144,16 +144,10 @@
>
{{ $t('minPasswordLength') }}
</div>
<div
v-if="passwordInvalid && !registering"
class="input-error"
>
{{ $t('minPasswordLengthLogin') }}
</div>
</div>
<div
v-if="registering"
class="form-group mb-4"
class="form-group"
>
<label
v-once
@@ -162,7 +156,7 @@
<input
id="confirmPasswordInput"
v-model="passwordConfirm"
class="form-control dark input-with-error"
class="form-control input-with-error"
type="password"
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
@@ -173,26 +167,30 @@
>
{{ $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 w-100 mb-4"
:disabled="!(emailValid && passwordValid && passwordConfirmValid)"
class="btn btn-info"
:disabled="signupFormInvalid"
>
{{ $t('continue') }}
{{ $t('joinHabitica') }}
</button>
<button
v-if="!registering"
v-once
type="submit"
class="btn btn-info w-100 mb-4"
:disabled="!usernameValid || !passwordValid"
class="btn btn-info"
>
{{ $t('login') }}
</button>
<div>
<div class="toggle-links">
<router-link
v-if="registering"
:to="{name: 'login'}"
@@ -200,7 +198,7 @@
>
<a
v-once
class="white"
class="toggle-link"
v-html="$t('alreadyHaveAccountLogin')"
></a>
</router-link>
@@ -211,7 +209,7 @@
>
<a
v-once
class="white"
class="toggle-link"
v-html="$t('dontHaveAccountSignup')"
></a>
</router-link>
@@ -223,66 +221,45 @@
id="forgot-form"
@submit.prevent="handleSubmit"
>
<div>
<div class="text-center">
<div>
<a
href="/static/home"
class="svg-icon habitica-logo mx-auto mb-4"
<div class="svg-icon gryphon"></div>
</div>
<div>
<div
class="svg-icon habitica-logo"
v-html="icons.habiticaIcon"
></a>
></div>
</div>
<div class="header">
<h2
v-once
class="text-center"
>
<h2 v-once>
{{ $t('emailNewPass') }}
</h2>
<p
v-once
class="purple-600 text-left"
>
<p v-once>
{{ $t('forgotPasswordSteps') }}
</p>
</div>
<div
class="form-group"
:class="{
'mb-2': usernameIssues.length > 0,
'mb-4': usernameIssues.length === 0,
}"
</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')"
>
<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>
<div class="text-center">
<div
v-for="issue in usernameIssues"
:key="issue"
class="input-error mb-2"
v-once
class="btn btn-info"
@click="forgotPasswordLink()"
>
{{ issue }}
</div>
<div class="text-center">
<button
class="btn btn-info w-100"
:disabled="!username || usernameIssues.length > 0"
@click="forgotPasswordLink()"
>
{{ $t('sendLink') }}
</button>
{{ $t('sendLink') }}
</div>
</div>
</form>
@@ -293,11 +270,10 @@
>
<div class="text-center">
<div>
<a
href="/static/home"
class="svg-icon habitica-logo mx-auto mb-4"
<div
class="svg-icon habitica-logo"
v-html="icons.habiticaIcon"
></a>
></div>
</div>
<div class="header">
<h2>{{ $t('passwordResetPage') }}</h2>
@@ -353,6 +329,15 @@
</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>
@@ -369,10 +354,30 @@
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
@import '@/assets/scss/forms.scss';
@import '@/assets/scss/privacy.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;
}
}
@media only screen and (max-width: 768px) {
#login-form {
width: 100% !important;
}
.form-group {
padding-left: .5em;
padding-right: .5em;
@@ -383,28 +388,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-500;
color: $purple-400;
}
::-moz-placeholder { /* Firefox 19+ */
color: $purple-500;
color: $purple-400;
}
:-ms-input-placeholder { /* IE 10+ */
color: $purple-500;
color: $purple-400;
}
:-moz-placeholder { /* Firefox 18- */
color: $purple-500;
color: $purple-400;
}
::placeholder { // Standard browsers
color: $purple-500;
color: $purple-400;
}
#login-form, #forgot-form, #reset-password-set-new-one-form {
margin: 0 auto;
width: 448px;
height: 700px;
width: 40em;
padding-top: 5em;
padding-bottom: 4em;
position: relative;
@@ -412,23 +417,39 @@
.header {
h2 {
font-size: 24px;
color: $white;
}
p {
line-height: 1.714;
}
color: $white;
}
.gryphon {
background-size: cover;
color: $white;
height: 69.4px;
margin: 0 auto;
width: 63.2px;
}
.habitica-logo {
width: 145px;
width: 175px;
height: 64px;
margin: 2em auto 0;
z-index: 0;
}
label {
color: $white;
font-weight: bold;
line-height: 1.714;
}
input {
margin-bottom: 2em;
border-radius: 2px;
background-color: #432874;
border-color: transparent;
height: 50px;
color: $white;
}
.input-with-error.input-invalid {
@@ -476,10 +497,54 @@
}
}
#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;
@@ -504,13 +569,14 @@
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.714;
line-height: 1.14;
color: #fff;
}
@@ -521,7 +587,7 @@
top: 50%;
width: 9999px;
height: 1px;
background: $purple-400;
background: #fff;
}
.strike > span:before {
@@ -537,24 +603,26 @@
<script>
import axios from 'axios';
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/es/lib/isEmail';
import PrivacyBanner from '@/components/header/banners/privacy';
import notifications from '@/mixins/notifications';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { buildAppleAuthUrl } from '../../libs/auth';
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
import accountCreation from '@/mixins/accountCreation';
import exclamation from '@/assets/svg/exclamation.svg?raw';
import habiticaIcon from '@/assets/svg/habitica-logo.svg?raw';
import gryphon from '@/assets/svg/gryphon.svg?raw';
import habiticaIcon from '@/assets/svg/logo-horizontal.svg?raw';
import googleIcon from '@/assets/svg/google.svg?raw';
import appleIcon from '@/assets/svg/apple_black.svg?raw';
export default {
components: {
PrivacyBanner,
},
mixins: [accountCreation, notifications, sanitizeRedirect],
mixins: [sanitizeRedirect],
data () {
const data = {
username: '',
email: '',
password: '',
passwordConfirm: '',
forgotPassword: false,
resetPasswordSetNewOneData: {
hasError: null,
@@ -565,6 +633,7 @@ export default {
data.icons = Object.freeze({
exclamation,
gryphon,
habiticaIcon,
googleIcon,
appleIcon,
@@ -585,6 +654,14 @@ 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;
@@ -593,6 +670,28 @@ 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: {
@@ -630,11 +729,71 @@ 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,
});
@@ -642,6 +801,31 @@ 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;
@@ -656,7 +840,7 @@ export default {
},
handleSubmit () {
if (this.registering) {
this.proceed('local');
this.register();
return;
}
@@ -712,20 +896,6 @@ 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>

View File

@@ -1,267 +0,0 @@
<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>

View File

@@ -321,8 +321,8 @@ export default {
return null;
},
petClass () {
const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment()
.isBetween(event.start, event.end));
const foolEvent = this.currentEventList?.find(event => moment()
.isBetween(event.start, event.end) && event.aprilFools);
if (foolEvent) {
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
}

View File

@@ -43,11 +43,9 @@ 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,
});
},

View File

@@ -72,40 +72,32 @@
</div>
<div class="col-12 col-md-6 text-right">
<div
class="box member-count p-2"
class="box member-count"
@click="showMemberModal()"
>
<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
class="svg-icon member-icon"
v-html="icons.memberIcon"
></div>
{{ challenge.memberCount }}
<div
v-once
class="details"
>
{{ $t('participantsTitle') }}
</div>
</div>
<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 class="box">
<div
class="svg-icon gem-icon"
v-html="icons.gemIcon"
></div>
{{ challenge.prize || 0 }}
<div
v-once
class="details"
>
{{ $t('prize') }}
</div>
</div>
</div>
@@ -312,6 +304,7 @@
.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);
@@ -321,88 +314,22 @@
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;
}
}
}

View File

@@ -4,7 +4,6 @@
id="close-challenge-modal"
:title="$t('endChallenge')"
size="md"
:hide-header="false"
>
<div
slot="modal-header"
@@ -16,9 +15,6 @@
>
{{ $t('endChallenge') }}
</h2>
<close-x
@close="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
/>
</div>
<div class="row text-center">
<span
@@ -32,67 +28,28 @@
class="col-12"
>
<div class="col-12">
<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 class="support-habitica">
<!-- @TODO: Add challenge achievement badge here-->
</div>
</div>
<div class="col-12">
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
</div>
<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 class="col-12">
<member-search-dropdown
:text="winnerText"
:members="members"
:challenge-id="challengeId"
@member-selected="selectMember"
/>
</div>
<div class="col-12">
<button
class="btn award-winner-btn"
:class="{'has-winner': winner._id}"
:disabled="!winner._id"
v-once
class="btn btn-primary"
@click="closeChallenge"
>
<span>{{ $t('awardWinners') }}</span>
<div
class="gem-icon"
v-html="icons.gem"
></div>
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
{{ $t('awardWinners') }}
</button>
</div>
</span>
@@ -103,27 +60,14 @@
</div>
</div>
<div class="col-12">
<strong
v-once
class="delete-challenge-text"
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
</div>
<div
v-once
class="col-12 refund-text"
>
{{ $t('deleteChallengeRefundDescription') }}
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
</div>
<div class="col-12">
<button
v-once
class="btn btn-danger delete-challenge-btn"
class="btn btn-danger"
@click="deleteChallenge()"
>
<div
class="svg-icon color delete-icon"
v-html="icons.deleteIcon"
></div>
{{ $t('deleteChallenge') }}
</button>
</div>
@@ -138,7 +82,6 @@
<style lang='scss'>
@import '@/assets/scss/colors.scss';
@import '@/assets/scss/button.scss';
#close-challenge-modal {
h2 {
@@ -151,190 +94,26 @@
.header-wrap {
width: 100%;
padding-top: 32px;
position: relative;
padding-top: 2em;
}
.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;
.support-habitica {
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
width: 325px;
height: 89px;
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: 1.5em;
}
.col-12:first-child {
margin-top: 0;
margin-top: 2em;
}
.or {
@@ -344,39 +123,21 @@
margin-right: auto;
margin-left: auto;
font-weight: bold;
color: $gray-100;
}
}
</style>
<script>
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';
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
export default {
components: {
closeX,
memberSearchDropdown,
},
props: ['challengeId', 'members', 'prize', 'flagCount'],
data () {
return {
winner: {},
searchTerm: '',
showResults: false,
filteredMembers: [],
icons: Object.freeze({
search: searchIcon,
deleteIcon,
gem: gemIcon,
endChallengeBadge,
gemsOrange,
gemsPurple,
}),
};
},
computed: {
@@ -389,35 +150,8 @@ export default {
},
},
methods: {
searchMembers () {
if (!this.searchTerm) {
this.filteredMembers = [];
return;
}
const searchLower = this.searchTerm.toLowerCase().replace('@', '');
this.filteredMembers = this.members.filter(member => {
const username = member.auth?.local?.username || '';
const displayName = member.profile?.name || '';
return username.toLowerCase().includes(searchLower)
|| displayName.toLowerCase().includes(searchLower);
}).slice(0, 10);
},
getMemberDisplayName (member) {
if (member.auth?.local?.username) {
return `@${member.auth.local.username}`;
}
return member.profile?.name || '';
},
selectMember (member) {
this.winner = member;
this.searchTerm = this.getMemberDisplayName(member);
this.showResults = false;
},
handleBlur () {
setTimeout(() => {
this.showResults = false;
}, 200);
},
async closeChallenge () {
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {

View File

@@ -5,7 +5,7 @@
:size="editing ? 'lg' : 'md'"
:hide-header="true"
:hide-footer="true"
:modal-class="{'page-2': !editing}"
:modal-class="{'page-2':modalPage > 1 && !editing}"
:no-close-on-esc="!editing"
:no-close-on-backdrop="!editing"
>
@@ -13,6 +13,20 @@
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"
@@ -20,6 +34,7 @@
{{ $t('editAvatar') }}
</h2>
<div
v-if="modalPage > 1"
class="avatar-section d-flex justify-content-center"
:class="{'page-2': modalPage === 2}"
>
@@ -418,7 +433,8 @@
</div>
<div
v-if="!editing"
class="section d-flex justify-content-center justin-outer-section top"
class="section d-flex justify-content-center justin-outer-section"
:class="{top: modalPage > 1}"
>
<div class="justin-section d-flex align-items-center">
<div class="featured-label">
@@ -443,8 +459,17 @@
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 v-html="$t('justinIntroMessage1')"></p>
<p>{{ $t('justinIntroMessageAppearance') }}</p>
</div>
<div v-if="modalPage === 3">
<p v-once>
@@ -459,12 +484,25 @@
</div>
</div>
<div
v-if="!editing"
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)"
class="section container footer"
>
<div class="footer-left">
<div
v-if="modalPage > 2"
v-if="modalPage > 1"
class="prev-outer"
@click="prev()"
>
@@ -481,6 +519,10 @@
</div>
</div>
<div class="footer-center text-center circles">
<div
class="circle"
:class="{active: modalPage === 1}"
></div>
<div
class="circle"
:class="{active: modalPage === 2}"
@@ -949,6 +991,7 @@ 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';
@@ -982,6 +1025,7 @@ export default {
extraSettings,
hairSettings,
skinSettings,
usernameForm,
Sprite,
},
mixins: [guide, notifications, avatarEditorUtilities],
@@ -1009,7 +1053,7 @@ export default {
arrowLeft,
close: svgClose,
}),
modalPage: 2,
modalPage: 1,
activeTopPage: 'body',
activeSubPage: 'size',
taskCategories: [],

View File

@@ -82,7 +82,9 @@
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"

View File

@@ -1,126 +0,0 @@
<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>

View File

@@ -1,43 +1,37 @@
<template>
<div
class="notification d-flex justify-content-center align-items-center"
class="notification d-flex flex-column justify-content-center text-center"
>
<img
src="@/assets/images/gifts_start.svg"
class="gift-start"
alt=""
<strong
v-once
class="mx-auto mb-2"
>
<div class="content-wrapper d-flex flex-column justify-content-center text-center">
<strong
v-once
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') }}
</button>
</div>
<img
src="@/assets/images/gifts_start.svg"
class="gift-end"
alt=""
{{ $t('g1g1') }}
</strong>
<small
v-once
class="mx-4 mb-3"
>
{{ $t('g1g1Details') }}
</small>
<div
class="close-x"
@click="remove()"
class="btn-secondary mx-auto d-flex"
@click="showSelectUser()"
>
<div
class="svg-icon svg-close"
v-once
class="m-auto"
>
{{ $t('sendGift') }}
</div>
</div>
<div
class="notification-remove"
@click.stop="remove()"
>
<div
v-once
class="svg-icon"
v-html="icons.close"
></div>
</div>
@@ -47,89 +41,51 @@
<style lang='scss' scoped>
@import '@/assets/scss/colors.scss';
small {
small, strong {
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/gifts_bg.svg');
background-size: cover;
background-position: center;
background-image: url('@/assets/images/g1g1-notif.png');
height: 10rem;
padding: 0;
padding: 3rem;
position: relative;
overflow: hidden;
white-space: normal;
cursor: pointer;
}
.content-wrapper {
flex: 1;
padding: 2rem;
z-index: 1;
}
.gift-start {
height: 96px;
width: auto;
.notification-remove {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 0;
}
width: 18px;
height: 18px;
padding: 4px;
right: 24px;
top: 24px;
.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-icon {
width: 10px;
height: 10px;
}
}
.svg-close {
width: 18px;
height: 18px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
.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;
}
</style>
<script>
import closeIcon from '@/assets/svg/close-white.svg?raw';
import closeIcon from '@/assets/svg/close-teal.svg?raw';
import { mapActions } from '@/libs/store';
export default {
props: ['notification', 'eventKey'],
props: ['notification'],
data () {
return {
icons: Object.freeze({
@@ -138,11 +94,11 @@ export default {
};
},
methods: {
...mapActions({
readNotification: 'notifications:readNotification',
}),
remove () {
if (this.eventKey) {
window.sessionStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
}
this.$emit('notification-removed');
this.readNotification({ notificationId: this.notification.id });
},
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');

View File

@@ -71,7 +71,7 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
this.$router.push({ name: 'achievements' });
},
},
};

View File

@@ -43,7 +43,7 @@ export default {
},
methods: {
action () {
this.$router.push(`/profile/${this.$store.state.user.data._id}#stats`);
this.$router.push({ name: 'stats' });
},
},
};

View File

@@ -49,12 +49,6 @@
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"
@@ -120,7 +114,6 @@
<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';
@@ -158,7 +151,6 @@ 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,
@@ -186,14 +178,17 @@ 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',
@@ -212,10 +207,7 @@ export default {
};
},
computed: {
...mapState({
user: 'user.data',
currentEventList: 'worldState.data.currentEventList',
}),
...mapState({ user: 'user.data' }),
notificationsOrder () {
// Returns a map of NOTIFICATION_TYPE -> POSITION
const orderMap = {};
@@ -294,9 +286,9 @@ export default {
return notifications;
},
// The total number of notification, shown inside the dropdown
notificationsCount () {
const g1g1Count = this.shouldShowG1g1 ? 1 : 0;
return this.notifications.length + g1g1Count;
return this.notifications.length;
},
hasUnseenNotifications () {
return this.notifications.some(notification => (notification.seen === false));
@@ -307,30 +299,6 @@ 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);
@@ -396,9 +364,6 @@ export default {
isActionable (notification) {
return this.actionableNotifications.indexOf(notification.type) !== -1;
},
handleG1g1Removed () {
this.g1g1Hidden = true;
},
},
};

View File

@@ -176,12 +176,7 @@ export default {
}
},
showProfile (startingPage) {
const userId = this.$store.state.user.data._id;
let path = `/profile/${userId}`;
if (startingPage !== 'profile') {
path += `#${startingPage}`;
}
this.$router.push(path);
this.$router.push({ name: startingPage });
},
toLearnMore () {
this.$router.push({ name: 'subscription' });

View File

@@ -454,14 +454,17 @@ 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;
if (!username) return false;
const usernamePattern = new RegExp(`@${escapeRegExp(username)}(?:\\b|(?=[^a-zA-Z0-9_]))`, 'i');
message.highlight = usernamePattern.test(message.text);
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
message.highlight = new RegExp(pattern, 'i').test(message.text);
return message.highlight;
},
flagCountDescription () {

View File

@@ -197,7 +197,9 @@
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"

View File

@@ -1,122 +0,0 @@
<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>

View File

@@ -12,12 +12,14 @@
class="staff col-6 p-0"
>
<div class="d-flex">
<div
<router-link
class="title"
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
>
{{ user.name }}
</div>
</router-link>
<div
v-if="user.type === 'Staff'"
class="svg-icon staff-icon ml-1"
v-html="icons.tierStaff"
></div>

View File

@@ -851,7 +851,7 @@ export default {
return;
}
if (this.genericPurchase) {
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.purchased(this.item.text);
}
}

View File

@@ -96,7 +96,6 @@ export default {
notificationTopY: '0px',
preventMultipleWatchExecution: false,
eventPromoBannerHeight: null,
privacyBannerHeight: null,
sleepingBannerHeight: null,
warningBannerHeight: null,
};
@@ -115,10 +114,6 @@ export default {
notificationBannerHeight () {
let scrollPosToCheck = 56;
if (this.privacyBannerHeight) {
scrollPosToCheck += this.privacyBannerHeight;
}
if (this.warningBannerHeight) {
scrollPosToCheck += this.warningBannerHeight;
}
@@ -169,9 +164,6 @@ 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();
@@ -347,7 +339,6 @@ 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();

View File

@@ -16,7 +16,7 @@
.static-view p {
padding-top: 100px;
font-size: 2em;
font-size: 2em
}
</style>
@@ -26,19 +26,13 @@ export default {
async mounted () {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const reqParams = { code: urlParams.get('code'), allowRegister: false };
const reqParams = { code: urlParams.get('code') };
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>

View File

@@ -120,9 +120,9 @@
>
<ul>
<li>
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'phillipthelen'}) }})
- Developer
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
@@ -133,6 +133,10 @@
{{ $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
@@ -142,13 +146,8 @@
- Mobile Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Kalista'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'fizself', realName: 'Hafiz'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'hafizzle'}) }})
- Developer
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
- Mobile Developer
</li>
</ul>
<p v-html="$t('commGuidePara013')"></p>
@@ -157,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, Cantras, and heyeilatan.
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
</em>
</p>
<h2 id="final">

View File

@@ -117,6 +117,18 @@
</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
@@ -303,10 +315,12 @@
<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],
@@ -339,11 +353,13 @@ 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) {
return this.$router.push('/register');
this.modalOption = 'static';
return this.$root.$emit('bv::show::modal', 'group-plan');
}
if (this.upgradingGroup._id) {
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });

View File

@@ -8,117 +8,124 @@
<br />
<a href="https://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
</noscript>
<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}) }}
<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') }}
</p>
</div>
<div class="w-33 ml-5">
<h3 class="text-center">
{{ $t('singUpForFree') }}
</h3>
<form
class="form pb-0"
@submit.prevent.stop="proceed('local')"
<input
id="usernameInput"
v-model="username"
class="form-control input-with-error"
type="text"
:placeholder="$t('username')"
:class="{'input-valid': usernameValid, 'input-invalid': usernameInvalid}"
>
<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
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
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>
<!-- 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>
<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>
<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="svg-icon social-icon"
v-html="icons.googleIcon"
></div>
<span>{{ $t('signUpWithSocial', {social: 'Google'}) }}</span>
</button>
<button
class="social-button"
@click="socialAuth('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 class="col-12">
@@ -128,218 +135,218 @@
></div>
</div>
</div>
</div>
</div>
<div
id="gamify-life"
class="purple-2"
>
<div class="container-fluid">
<div
id="gamify-life"
class="bg-purple-100 white"
>
<div class="container-fluid">
<div
class="pixel-horizontal svg-icon"
v-html="icons.pixelHorizontal"
></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>
</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>
</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="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">
<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">
<div
class="spacer svg-icon"
v-html="icons.spacer"
></div>
<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
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>
class="spacer svg-icon"
v-html="icons.spacer"
></div>
</div>
<div class="container-fluid">
<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>
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>
</div>
<div class="container-fluid">
<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="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>
</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="pixel-horizontal-3 svg-icon"
v-html="icons.pixelHorizontal3"
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
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>
<div class="container-fluid">
<div class="row seamless_stars_varied_opacity_repeat"></div>
</div>
</div>
</div>
@@ -347,27 +354,16 @@
<style lang='scss'>
@import '@/assets/scss/static.scss';
#front {
.form-text a {
color: $white !important;
}
.privacy-banner p {
font-size: 14px;
}
#front .form-text a {
color: $white !important;
}
</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;
@@ -378,10 +374,22 @@
padding-bottom: 5em;
}
.custom-control-label, h1, h2, h3, h4, h5 {
.purple-1, .purple-2, .purple-3, .purple-4, 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;
}
@@ -415,26 +423,18 @@
color: $header-dark-background;
}
h1, h2, h3, h4, h5, h6, .strike > span {
h1, h2, h3, h4, h5, h6, button, .strike > span, input {
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 {
@media only screen and (min-width: 992px) {
margin-left: 15%;
}
margin: 0 auto;
display: block;
}
h1 {
@@ -458,7 +458,6 @@
transition: .5s;
span {
font-weight: 700;
transition: none;
}
}
@@ -522,6 +521,64 @@
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 {
@@ -722,14 +779,20 @@
}
.input-error {
color: $white;
font-size: 90%;
width: 100%;
margin-bottom: 1em;
}
</style>
<script>
import notifications from '@/mixins/notifications';
import accountCreation from '@/mixins/accountCreation';
import PrivacyBanner from '@/components/header/banners/privacy';
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 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';
@@ -750,10 +813,7 @@ 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 {
components: {
PrivacyBanner,
},
mixins: [accountCreation, notifications],
mixins: [sanitizeRedirect],
data () {
return {
icons: Object.freeze({
@@ -777,17 +837,132 @@ export default {
thenewyorktimes,
}),
userCountInMillions: 4,
username: '',
password: '',
passwordConfirm: '',
email: '',
usernameIssues: [],
};
},
computed: {
emailValid () {
if (this.email.length < 1) return false;
return isEmail(this.email);
},
emailInvalid () {
if (this.email.length < 1) return false;
return !isEmail(this.email);
},
usernameValid () {
if (this.username.length < 1) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.username.length < 1) return false;
return !this.usernameValid;
},
passwordValid () {
if (this.password.length <= 0) return false;
return this.password.length >= MINIMUM_PASSWORD_LENGTH;
},
passwordInvalid () {
if (this.password.length <= 0) return false;
return this.password.length < MINIMUM_PASSWORD_LENGTH;
},
passwordConfirmValid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm === this.password;
},
passwordConfirmInvalid () {
if (this.passwordConfirm.length <= 3) return false;
return this.passwordConfirm !== this.password;
},
signupFormInvalid () {
return this.usernameInvalid
|| this.emailInvalid
|| this.passwordInvalid
|| this.passwordConfirmInvalid;
},
},
watch: {
username () {
this.validateUsername(this.username);
},
},
mounted () {
hello.init({
google: import.meta.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
this.$store.dispatch('common:setTitle', {
fullTitle: 'Habitica - Gamify Your Life',
});
},
methods: {
// eslint-disable-next-line func-names
validateUsername: debounce(function (username) {
if (username.length < 1) {
return;
}
this.$store.dispatch('auth:verifyUsername', {
username: this.username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
// @TODO this is totally duplicate from the registerLogin component
async register () {
let groupInvite = '';
if (this.$route.query && this.$route.query.p) {
groupInvite = this.$route.query.p;
}
if (this.$route.query && this.$route.query.groupInvite) {
groupInvite = this.$route.query.groupInvite;
}
await this.$store.dispatch('auth:register', {
username: this.username,
email: this.email,
password: this.password,
passwordConfirm: this.passwordConfirm,
groupInvite,
});
const redirect = this.sanitizeRedirect(this.$route.query.redirectTo);
window.location.href = redirect;
},
playButtonClick () {
this.$router.push('/register');
},
// @TODO: Abstract hello in to action or lib
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
try {
await hello(network).logout();
} catch (e) {} // eslint-disable-line
const redirectUrl = `${window.location.protocol}//${window.location.host}`;
const auth = await hello(network).login({
scope: 'email',
// explicitly pass the redirect url or it might redirect to /home
redirect_uri: redirectUrl, // eslint-disable-line camelcase
});
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
}
},
},
};
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<div>
<chat-banner />
<static-header
v-if="showContentWrap && !loginFlow"
v-if="showContentWrap"
:class="{
'home-header': ['home', 'front'].indexOf($route.name) !== -1,
'white-header': $route.name === 'plans'
@@ -15,23 +15,17 @@
<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" v-if="!loginFlow">
<div id="bottom-background">
<div class="seamless_mountains_demo_repeat"></div>
<div class="midground_foreground_extended2"></div>
</div>
@@ -269,16 +263,13 @@ 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>

View File

@@ -3,130 +3,49 @@
<div class="container-fluid">
<h1>Terms of Service</h1>
<p class="strong pagemeta">
Last Updated September 1, 2025.
Last Updated: December 14, 2021
</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.
>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.
</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 CAREFULLY.
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.
</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 individuals 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>
<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>
<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. 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>
<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>
<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>
<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>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>
<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>
<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>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>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>
@@ -134,113 +53,53 @@
<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>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>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 “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>
<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>
</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>
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>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>
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.
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUND 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>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>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>
<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>
<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 "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
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
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>
<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>
<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>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>
<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>
<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>
<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>
<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>
<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>
</div>
<!-- eslint-enable max-len -->
</template>

View File

@@ -86,8 +86,8 @@
v-if="taskList.length > 0"
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
scrollSensitivity="64"
:options="{disabled: activeFilter.label === 'scheduled' || !canBeDragged(),
scrollSensitivity: 64}"
:delay-on-touch-only="true"
:delay="100"
@update="taskSorted"

View File

@@ -21,9 +21,11 @@
>
<draggable
v-model="checklist"
handle=".grippy"
filter=".task-dropdown"
:disabled="disabled"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
>
<div

View File

@@ -26,17 +26,12 @@
: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"
@@ -126,14 +121,6 @@
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;
@@ -194,10 +181,6 @@ export default {
hoverText: {
type: String,
},
disabled: {
type: Boolean,
default: false,
},
},
data () {
return {

View File

@@ -1126,12 +1126,7 @@ export default {
this.loadUser();
this.oldTitle = this.$store.state.title;
this.handleExternalLinks();
// 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.selectPage(this.startingPage);
this.$root.$on('habitica:report-profile-result', () => {
this.loadUser();
});
@@ -1216,15 +1211,10 @@ export default {
},
selectPage (page) {
this.selectedPage = page || 'profile';
const profileUserId = this.userId || this.userLoggedIn._id;
let newPath = `/profile/${profileUserId}`;
if (page !== 'profile') {
newPath += `#${page}`;
}
window.history.replaceState(null, null, newPath);
window.history.replaceState(null, null, '');
this.$store.dispatch('common:setTitle', {
section: this.$t('user'),
subSection: this.$t(page),
subSection: this.$t(this.startingPage),
});
},
getNextIncentive () {

View File

@@ -2,24 +2,26 @@ 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 AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
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
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;
}
const REQUIRED_FIELDS = ['hitType', 'eventCategory', 'eventAction'];
const ALLOWED_HIT_TYPES = [
'pageview',
'screenview',
'event',
'transaction',
'item',
'social',
'exception',
'timing',
];
function _doesNotHaveRequiredFields (properties) {
if (!isEqual(keys(pick(properties, REQUIRED_FIELDS)), REQUIRED_FIELDS)) {
@@ -32,6 +34,17 @@ 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;
@@ -62,26 +75,24 @@ function _gatherUserStats (properties) {
if (user.purchased.plan.planId) properties.subscription = user.purchased.plan.planId;
}
export function safeSetup (userId) {
if (analyticsLoading || analyticsReady) return;
analyticsLoading = true;
amplitude.getInstance().init(AMPLITUDE_KEY, userId);
analyticsReady = true;
analyticsLoading = false;
export function setUser () {
const store = getStore();
const user = store.state.user.data;
amplitude.getInstance().setUserId(user._id);
window.ga('set', { userId: user._id });
}
export function track (properties, options = {}) {
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);
@@ -90,15 +101,45 @@ 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);
}

View File

@@ -21,7 +21,7 @@ function loadLocale (i18nData) {
script.type = 'text/javascript';
script.text = i18nData.momentLang;
head.appendChild(script);
moment.updateLocale(language.momentLangCode);
moment.locale(language.momentLangCode);
}
}

View File

@@ -1,16 +1,7 @@
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 };
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;
const env = { userName: user.auth.local.username, displayName: user.profile.name };
return habiticaMarkdown.render(String(text), env);
}

View File

@@ -30,8 +30,8 @@ export default [
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
},
{
name: 'fizself',
name: 'heyeilatan',
type: 'Staff',
uuid: 'e39ea3eb-28d2-48da-8568-7a5b0e64498e',
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
},
];

View File

@@ -13,6 +13,9 @@ 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';
@@ -47,6 +50,7 @@ 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') {

View File

@@ -1,128 +0,0 @@
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;
},
},
};

View File

@@ -98,10 +98,16 @@
}
.settings-content {
flex: 0 0 751px;
flex: 0 0 732px;
max-width: unset;
::v-deep {
line-height: 1.71;
.small {
line-height: 1.33;
}
table td {
padding: 0.5rem;
}
@@ -117,14 +123,6 @@
width: 23%;
}
small {
line-height: 1.33;
}
.settings-label, .settings-value, a, p {
line-height: 1.71;
}
.input-area .settings-label {
width: unset;
}

View File

@@ -5,7 +5,7 @@
>
<button
v-if="!hideSave"
class="btn btn-save mb-2"
class="btn btn-save"
:class="primaryButtonColor ?? 'btn-primary'"
type="submit"
:disabled="disableSave"
@@ -61,4 +61,8 @@ export default {
margin-top: 1.5rem;
}
}
.btn-save {
margin-bottom: 1rem;
}
</style>

View File

@@ -16,7 +16,6 @@
<table class="table">
<user-id-row />
<user-data-row />
<privacy-preferences-row />
<tr>
<td colspan="3">
</td>
@@ -46,7 +45,6 @@
<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';
@@ -58,7 +56,6 @@ export default {
ApiRow,
UserDataRow,
UserIdRow,
PrivacyPreferencesRow,
},
mounted () {
this.$store.dispatch('common:setTitle', {

View File

@@ -1,202 +0,0 @@
<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>

View File

@@ -15,7 +15,6 @@
<external-link-modal />
<birthday-modal />
<template v-if="isUserLoaded">
<privacy-banner />
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
@@ -119,12 +118,11 @@ 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 PrivacyBanner from '@/components/header/banners/privacy';
import BirthdayBanner from '@/components/header/banners/birthdayBanner';
import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store';
@@ -161,7 +159,6 @@ export default {
GemsPromoBanner,
GiftPromoBanner,
BirthdayBanner,
PrivacyBanner,
notificationsDisplay,
BuyModal,
SelectMembersModal,
@@ -263,24 +260,18 @@ export default {
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
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.setUser();
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',
{

View File

@@ -24,8 +24,6 @@ const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
// Tasks
@@ -78,6 +76,12 @@ const router = new VueRouter({
// NOTE: when adding a new route entry make sure to implement the `common:setTitle` action
// in the route component to set a specific subtitle for the page.
routes: [
{
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{
name: 'login', path: '/login', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{ name: 'logout', path: '/logout', component: Logout },
{
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
@@ -90,9 +94,6 @@ const router = new VueRouter({
path: '/profile/:userId',
props: true,
},
{ name: 'profile', path: '/user/profile' },
{ name: 'stats', path: '/user/stats' },
{ name: 'achievements', path: '/user/achievements' },
{
path: '/inventory',
component: InventoryContainer,
@@ -221,28 +222,6 @@ const router = new VueRouter({
},
],
},
{
name: 'groupAdmin',
path: 'groups',
component: GroupAdminPage,
meta: {
privilegeNeeded: [ // any one of these is enough to give access
'groupSupport',
],
},
children: [
{
name: 'groupAdminGroup',
path: ':groupId',
component: GroupAdminGroupPage,
meta: {
privilegeNeeded: [
'groupsSupport',
],
},
},
],
},
{
name: 'blockers',
path: 'blockers',
@@ -372,10 +351,6 @@ router.beforeEach(async (to, from, next) => {
if (to.params.startingPage !== undefined) {
startingPage = to.params.startingPage;
}
// Check if there's a hash in the URL for stats or achievements
if (to.hash === '#stats' || to.hash === '#achievements') {
startingPage = to.hash.substring(1);
}
if (from.name === null) {
store.state.postLoadModal = `profile/${to.params.userId}`;
return next({ name: 'tasks' });
@@ -396,18 +371,10 @@ router.beforeEach(async (to, from, next) => {
}
if ((to.name === 'stats' || to.name === 'achievements' || to.name === 'profile') && from.name !== null) {
const userId = store.state.user.data._id;
let redirectPath = `/profile/${userId}`;
if (to.name === 'stats') {
redirectPath += '#stats';
} else if (to.name === 'achievements') {
redirectPath += '#achievements';
}
router.app.$emit('habitica:show-profile', {
userId,
startingPage: to.name,
fromPath: from.path,
toPath: redirectPath,
toPath: to.path,
});
return null;
}

View File

@@ -6,12 +6,13 @@ const StaticWrapper = () => import('@/components/static/staticWrapper');
const HomePage = () => import('@/components/static/home');
const AppleRedirectPage = () => import('@/components/static/appleRedirect');
const ChatSunsetFaq = () => import('@/components/static/chatSunsetFaq');
const ClearBrowserDataPage = () => import('@/components/static/clearBrowserData');
const CommunityGuidelinesPage = () => import('@/components/static/communityGuidelines');
const ContentScheduleFaq = () => import('@/components/static/contentScheduleFaq');
const ContactPage = () => import('@/components/static/contact');
const FAQPage = () => import('@/components/static/faq');
const ChatSunsetFaq = () => import('@/components/static/chatSunsetFaq');
const ContentScheduleFaq = () => import('@/components/static/contentScheduleFaq');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const FeaturesPage = () => import('@/components/static/features');
const GroupPlansPage = () => import('@/components/static/groupPlans');
// Commenting out merch page see
@@ -21,9 +22,6 @@ const NewsPage = () => import('@/components/static/newStuff');
const OverviewPage = () => import('@/components/static/overview');
const PressKitPage = () => import('@/components/static/pressKit');
const PrivacyPage = () => import('@/components/static/privacy');
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const TermsPage = () => import('@/components/static/terms');
export const STATIC_ROUTES = {
@@ -57,9 +55,6 @@ export const STATIC_ROUTES = {
{
name: 'features', path: 'features', component: FeaturesPage, meta: { requiresLogin: false },
},
{
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
},
{
name: 'groupPlans', path: 'group-plans', component: GroupPlansPage, meta: { requiresLogin: false },
},
@@ -67,7 +62,7 @@ export const STATIC_ROUTES = {
name: 'home', path: 'home', component: HomePage, meta: { requiresLogin: false },
},
{
name: 'login', path: '/login', component: RegisterLoginReset, meta: { requiresLogin: false },
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
},
{
name: 'news', path: 'new-stuff', component: NewsPage, meta: { requiresLogin: false },
@@ -84,21 +79,9 @@ export const STATIC_ROUTES = {
{
name: 'privacy', path: 'privacy', component: PrivacyPage, meta: { requiresLogin: false },
},
{
name: 'privacyReview', path: 'privacy-review', component: PrivacyPage, meta: { requiresLogin: false },
},
{
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{
name: 'terms', path: 'terms', component: TermsPage, meta: { requiresLogin: false },
},
{
name: 'termsReview', path: 'terms-review', component: TermsPage, meta: { requiresLogin: false },
},
{
name: 'username', path: '/username', component: RegisterUsername, meta: { requiresLogin: false },
},
{
name: 'notFound', path: 'not-found', component: NotFoundPage, meta: { requiresLogin: false },
},

View File

@@ -1,31 +0,0 @@
import axios from 'axios';
export async function searchUsers (store, payload) {
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
const response = await axios.get(url);
return response.data.data;
}
export async function getUserHistory (store, payload) {
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
const response = await axios.get(url);
return response.data.data;
}
export async function getSubscriptionPaymentDetails (store, payload) {
const url = `/api/v4/admin/user/${payload.userIdentifier}/subscription-payment-details`;
const response = await axios.get(url);
return response.data.data;
}
export async function getGroup (store, payload) {
const url = `/api/v4/admin/groups/${payload.groupId}`;
const response = await axios.get(url);
return response.data.data;
}
export async function updateGroup (store, payload) {
const url = `/api/v4/admin/groups/${payload.groupId || payload.group._id}`;
const response = await axios.put(url, payload.group);
return response.data.data;
}

View File

@@ -0,0 +1,13 @@
import axios from 'axios';
export async function searchUsers (store, payload) {
const url = `/api/v4/admin/search/${payload.userIdentifier}`;
const response = await axios.get(url);
return response.data.data;
}
export async function getUserHistory (store, payload) {
const url = `/api/v4/admin/user/${payload.userIdentifier}/history`;
const response = await axios.get(url);
return response.data.data;
}

Some files were not shown because too many files have changed in this diff Show More