Compare commits

..

104 Commits

Author SHA1 Message Date
Phillip Thelen e890223a02 cleanup 2026-02-05 11:28:20 +01:00
Phillip Thelen 9a767a9eeb allow task history to be omitted from task list calls 2026-02-05 11:17:19 +01:00
Phillip Thelen 0982c80376 allow negative userFields to be passed 2026-02-05 11:16:57 +01:00
Kalista Payne 5dd9711413 Update local dev MongoDB versions (#15334)
* chore(mongodb): update local dev MongoDB versions

* fix(github): update mongodb-github-action

* test(api): attempt to gather more detail on failures

* Revert "test(api): attempt to gather more detail on failures"

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* fix(mongo): start replica set

* run mongo as gh action service

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

* remove previous code, dont share mongo data folders on runtype (rs and docker)

* mongo docker for testing; align mongodb directory naming

* remove run-rs, add docker:aio script call, use healthcheck to initiate again

* merge docker-compose.yml, fix client port listening

* fix oudated healthcheck param

* chore(mongodb): update local dev MongoDB versions

* fix(github): update mongodb-github-action

* test(api): attempt to gather more detail on failures

* Revert "test(api): attempt to gather more detail on failures"

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* run mongo as gh action service

* fix(mongo): start replica set

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

* remove previous code, dont share mongo data folders on runtype (rs and docker)

* mongo docker for testing; align mongodb directory naming

* remove run-rs, add docker:aio script call, use healthcheck to initiate again

* merge docker-compose.yml, fix client port listening

* fix oudated healthcheck param

* fix(config): remove dup keys

* using npx vite during docker aio run

---------

Co-authored-by: negue <eugen.bolz@gmail.com>
2026-01-30 17:19:35 -06:00
Kalista Payne a542277a41 5.44.2 2026-01-26 11:46:03 -06:00
Phillip Thelen cdf8556fd6 Improve performance in production setup (#15594)
* build cached content files for mobile during gulp build

* load already cached content files during startup

* add option for mongoose to define minPoolSize

* cache client index.html for 10 minutes. Improves initial load times

* add option to auth to use lean version of user doc

* add a way to produce a heapdump from the command line

* fix lint
2026-01-26 11:45:44 -06:00
Kalista Payne 3d93390a7a fix(languages): update template characters 2026-01-23 13:09:14 -06:00
Kalista Payne 59f9cfa0f4 5.44.1 2026-01-23 13:03:30 -06:00
Kalista Payne 80d7804f69 fix(strings): use consistent template characters 2026-01-23 12:53:29 -06:00
Kalista Payne 4e5efe09a3 5.44.0 2026-01-22 11:53:20 -06:00
Fiz d42a597672 Merge pull request #15590 from HabitRPG/kalista/paypal-debug
PayPal debug
2026-01-21 16:11:41 -06:00
Phillip Thelen ea17b2e9c7 Rework how strings are localized (#15589)
* replace lodash template usage with micromustache

* remove function brackets from translations

* add newline

* remove old test

* split core translations from content translations

* fix directory not existing

* fix lint
2026-01-21 14:34:25 -06:00
Phillip Thelen f56708cd88 fix most customizations not being pinnable (#15578)
* fix most customizations not being pinnable

* set correct pinTypes

* fix(pinning): correct purchase types for base hair and mustaches

* automatically unpin purchased customizations

* ability to pin customization items

* Fix pin not showing on buy modal

* Pin on buy modal tweak

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-01-21 13:57:42 -06:00
Kalista Payne 005d14f6e8 5.43.4 2026-01-20 14:36:36 -06:00
Weblate c05a96ce6c Translated using Weblate (Polish)
Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 90.5% (183 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Portuguese)

Currently translated at 27.6% (70 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.8% (3318 of 3497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.8% (3317 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 94.0% (190 of 202 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 50.1% (1753 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.1% (238 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 79.3% (150 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (928 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Cezary Polakowski <arutha86@gmail.com>
Co-authored-by: Daniel Costa Carvalho <danielcostacarvalho@gmail.com>
Co-authored-by: Illana Beatriz Rocha de Oliveira <dev.illanabeatriz@gmail.com>
Co-authored-by: Lu dG <ludgs@outlook.fr>
Co-authored-by: Lyam Santos Peres <karinesanper@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Nina Łapaj <ninapaj@gmail.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Npc
Translation: Habitica/Tasks
2026-01-20 21:33:14 +01:00
Kalista Payne 8fdbfb9dc6 fix(lint): import order 2026-01-16 17:47:05 -06:00
Kalista Payne 057a642baa fix(logger): path 2026-01-16 17:43:54 -06:00
Kalista Payne 6c522157a7 feat(payments): log some anonymous data to chase a bug 2026-01-16 17:38:16 -06:00
Fiz ba9a1ab2a9 Merge pull request #15583 from HabitRPG/kalista/username-sanitize
Sanitize usernames come from social auth
2026-01-15 12:00:56 -06:00
Kalista Payne 4767461c4f 5.43.3 2026-01-14 14:19:54 -06:00
Weblate 847c97dc8f Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.0% (228 of 245 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Polish)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Danish)

Currently translated at 88.1% (178 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Danish)

Currently translated at 66.5% (183 of 275 strings)

Translated using Weblate (German)

Currently translated at 98.9% (3461 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Danish)

Currently translated at 92.5% (175 of 189 strings)

Translated using Weblate (Danish)

Currently translated at 9.0% (23 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.0% (200 of 202 strings)

Translated using Weblate (German)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Danish)

Currently translated at 62.5% (172 of 275 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 96.5% (195 of 202 strings)

Translated using Weblate (Japanese)

Currently translated at 98.4% (249 of 253 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.3% (3440 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (202 of 202 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Douglas Meneghetti <douglasrizzom@gmail.com>
Co-authored-by: Gabriela <gabisouzars5@gmail.com>
Co-authored-by: Helene Bæck Christensen <helene.baeck@gmail.com>
Co-authored-by: Henrique Ferreira <pedroferreira217.ph@gmail.com>
Co-authored-by: Illana Beatriz Rocha de Oliveira <dev.illanabeatriz@gmail.com>
Co-authored-by: Isabela de Carvalho <isabela.c.escritora@gmail.com>
Co-authored-by: Joanna K <joanna.kociolek0@gmail.com>
Co-authored-by: Kenvinn <kevinsavio514@gmail.com>
Co-authored-by: Lucas Rafaldini <lucas.rafaldini@gmail.com>
Co-authored-by: Luã Fhelyp Guimarães <fhelypg@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Lyam Santos Peres <karinesanper@gmail.com>
Co-authored-by: Maycon Douglas Silva Santos <maycondss@live.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Rodrigo Gonçalves Braga <rgbraga@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Thiago Braga <thibraga06@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: purea <g@agaric.eu>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/de/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/id/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/da/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/da/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/da/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/da/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-01-14 21:18:58 +01:00
Kalista Payne 215b26acac fix(auth): run the actual verify check 2026-01-13 17:01:09 -06:00
Phillip Thelen e223e7821a add config to disable ssl and base_url enforcement (#15587) 2026-01-13 16:44:23 -06:00
Fiz 8134fa7c00 Chat optimization (#15545)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown

* Show orb of rebirth confirmation modal after use (window refresh)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Fix amount of messages initially being shown

* PM_PER_PAGE set to 50

* Increases number of messages in inbox test

* Increases number of messages for inbox pagination test

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Add UUID validation for 'before' query parameter

* add party message stress test tool in admin panel

* lint

* add MAX_PM_COUNT of 400, admin tool for stress testing messages

* comment

* update stress test inbox message tool to use logged in user

* comment

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-01-13 14:43:09 -06:00
Kalista Payne 84208f612e 5.43.2 2026-01-09 10:43:36 -06:00
Hafiz 57e06334c0 Update allocation option label 2026-01-09 09:05:00 -06:00
Kalista Payne be695d25b3 5.43.1 2026-01-08 16:08:52 -06:00
Kalista Payne 77ee83f467 fix(privacy): ensure opt-in for analytics in modal 2026-01-08 15:40:09 -06:00
Kalista Payne 86556e346b 5.43.0 2026-01-08 14:22:27 -06:00
Weblate 2007a872c6 Merge branch 'origin/develop' into Weblate. 2026-01-08 21:20:01 +01:00
Weblate a8348038de Translated using Weblate (Italian)
Currently translated at 80.8% (2829 of 3497 strings)

Translated using Weblate (Italian)

Currently translated at 80.4% (2813 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 79.5% (2781 of 3497 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.6% (3310 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.3% (3438 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (French)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (French)

Currently translated at 93.0% (188 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (114 of 114 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Italian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.4% (3268 of 3497 strings)

Translated using Weblate (Korean)

Currently translated at 99.7% (930 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 72.1% (622 of 862 strings)

Translated using Weblate (Korean)

Currently translated at 75.8% (326 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 97.7% (129 of 132 strings)

Translated using Weblate (Korean)

Currently translated at 37.2% (92 of 247 strings)

Translated using Weblate (Korean)

Currently translated at 93.6% (104 of 111 strings)

Translated using Weblate (Korean)

Currently translated at 97.5% (909 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Korean)

Currently translated at 60.9% (173 of 284 strings)

Translated using Weblate (Korean)

Currently translated at 72.5% (312 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 49.7% (1739 of 3497 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Korean)

Currently translated at 63.7% (58 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 55.5% (160 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 34.8% (86 of 247 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 76.1% (188 of 247 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.7% (3243 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.6% (3240 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.4% (3233 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 72.7% (627 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (3229 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (3229 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3165 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3165 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Korean)

Currently translated at 70.7% (610 of 862 strings)

Translated using Weblate (Korean)

Currently translated at 71.3% (307 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 49.6% (1737 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 71.6% (618 of 862 strings)

Translated using Weblate (Romanian)

Currently translated at 56.5% (156 of 276 strings)

Translated using Weblate (Romanian)

Currently translated at 91.2% (104 of 114 strings)

Translated using Weblate (Romanian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (3432 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Turkish)

Currently translated at 63.9% (551 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.9% (3427 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3423 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.3% (218 of 244 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 77.2% (146 of 189 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.7% (88 of 91 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 95.4% (106 of 111 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 71.6% (197 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.9% (3495 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Turkish)

Currently translated at 87.3% (241 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2446 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.2% (3436 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 74.6% (206 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 74.6% (206 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 69.5% (192 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 65.9% (182 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 85.5% (797 of 932 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Douglas Meneghetti <douglasrizzom@gmail.com>
Co-authored-by: Frodo <Zatriel@outlook.com.br>
Co-authored-by: Israel Silva Araújo <yisrael.araujo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: John B <habitica.quarters574@passmail.net>
Co-authored-by: Lucas Lo <lucas1020.rabbit@gmail.com>
Co-authored-by: Luizo <t.czj2019@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Márcio Ramos Corrêa <marcio.ramos.correa@gmail.com>
Co-authored-by: Omar Bertolla <scaram@icloud.com>
Co-authored-by: Sefa Uğurlu <ugurlusefa2@gmail.com>
Co-authored-by: Seohee <ka011215@naver.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Trần Thu Trúc <ngw.nah@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ‌ <e0gla0f@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 張鈞崴 <qoo94230@gmail.com>
Co-authored-by: 강아름 <sick_kar@naver.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/id/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/id/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/id/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/id/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-01-08 20:28:08 +01:00
Hafiz 87bcd69979 stat allocation wording 2026-01-08 12:35:31 -06:00
Hafiz 8f8e84d0c7 Stat allocation selection lowercase/capitalization 2026-01-08 11:51:21 -06:00
Hafiz 2c18cb00cc capitalization 2026-01-08 11:27:35 -06:00
Hafiz daa0fd18c0 Remove "points" from task allocation dropdown item title 2026-01-07 19:30:20 -06:00
Kalista Payne 5c555cbf88 fix(auth): revert attempted perf tweak 2026-01-07 17:52:33 -06:00
Kalista Payne 7379c7b230 fix(auth): handle potential collisions on username 2026-01-07 17:26:42 -06:00
Kalista Payne c055537c38 fix(lint): whitespace 2026-01-07 16:49:22 -06:00
Kalista Payne 7559feec8e Revert "fix(lint): whitespace; also revert username fix for further QA"
This reverts commit dcd15a58ebf41d9e44c89c675419ecb7d36b946d.
2026-01-07 16:49:22 -06:00
Kalista Payne 43808696a8 fix(lint): whitespace; also revert username fix for further QA 2026-01-07 16:49:22 -06:00
Kalista Payne 72fb41c7e0 fix(lint): missing variable declaration 2026-01-07 16:45:19 -06:00
Kalista Payne 3bf18e09ed fix(auth): strip invalid characters during social reg 2026-01-07 16:45:19 -06:00
Kalista Payne 407a901883 feat(faq): add detail on stats and allocation 2026-01-07 15:30:37 -06:00
Kalista Payne 81a008906b fix(lint): max-len 2026-01-07 14:36:51 -06:00
Kalista Payne 992a978923 Squashed commit of the following:
commit dcd15a58ebf41d9e44c89c675419ecb7d36b946d
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Jan 7 14:11:29 2026 -0600

    fix(lint): whitespace; also revert username fix for further QA

commit 8af109f3c358418a778d4dc8387524840343f046
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Jan 5 15:04:30 2026 -0600

    fix(footer): better hover looks

commit 87518d335967a90d3049cfde9160b341cbf55417
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 15:10:14 2025 -0600

    fix(footer): change to flex layout for better alignment

commit 45ab33c1afaa5b34c56fb09cdcd0f4c12fee0893
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 13:03:56 2025 -0600

    fix(lint): whitespace

commit c9d20e86d29d89b881813a2579ec1ba61acc82cc
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 12:54:04 2025 -0600

    fix(lint): unused var and whitespace

commit 05dc11fd11a530b0b460a951a8be32fe6f4e190e
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 12:48:26 2025 -0600

    fix(footer): rearrange social block, clean up unused donate css

commit 131cda6bb99b4eb53fdadf551bfddc3f712e81e8
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 16:19:18 2025 -0600

    fix(login): move password flows into static wrapper

commit 563ed9fa02a34bbad53421795415e43c955813c1
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 16:02:41 2025 -0600

    fix(login): resetPassword not reset-password route name

commit d23b64a03ec48b5634f05b4500153ed54a0ba6e7
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 15:48:51 2025 -0600

    fix(login): add missing footer, remove dono button

commit 55e841252fbaafdccf75828b7ce87946c185792a
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 19 15:58:52 2025 -0600

    fix(passwords): standardize styling for reset pass form

commit 7e84a54f104191d6e6d2e86f25070847e1c1c674
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:21:50 2025 -0600

    fix(lint): missing variable declaration

commit 777f8f13d7b3b889b5637102b8258b20020c6c6d
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:17:19 2025 -0600

    fix(auth): strip invalid characters during social reg
2026-01-07 14:13:47 -06:00
Fiz a8062ad615 Implement stat allocation & auto stat allocation (#15551)
* fix(responsive): adjustments for small screens by @Hafizzle

* Fixed cut off modal issues

- Fixed limited time banner being cut off on smaller devices
- Fixed Pin on purchase quest modal being cut off

* Update order of views on Stats tab

* Update styling of stat boxes & add stat allocation info

* Live stat allocation & auto allocation implemented

- Update stat allocation UI/UX cont.
- Update stats live (versus pressing save)
- Dynamically show the distribute dropdown based on if auto allocate switch is enabled

* Remove duplicate string value, convert line endings

* Fix auto allocation toggle alignment

* lint fixes

* Auto allocate UI fixes

- remove info icon in each dropdown option, and replace info below each option
- disable auto allocate dropdown instead of hiding it
- removed radio buttons drop auto allocate options dropdown
- Points available & auto allocate text share same baseline now

* Stat allocation updates

* Assigned Stat

* lint fixes

* stat feedback UI updates

* tweak

* stat allocation UI tweaks

refactor allocation mode selection with selectList component & other UI tweaks

* Fix hover dropdown padding & stat allocation chip always showing

* Update stat allocation dropdown styling

* Remove bold text on hover

* Stat allocation by task dropdown

* remove empty lines

* show selected attribute on component itself

* task stat selection UI updates

* fix(cr): remove weird line endings

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-01-06 16:08:34 -06:00
Fiz 781a904583 Replace browser confirmations with confirmation modals (#15544)
* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* Confirmation prompts (replace browser confirmation prompts)

* lint

* lint

* Fix confirmation modal not showing for items w/gems

* Updated delete and confirmation modals

* lint

* Fix top bar on color on confirmation prompt clipping

* Show currency amount on purchase confirmation modals

* confirmation modals using existing components

* lint

* Broken task confirmation modal

* Replace class change confirmation with modal

* fix(style): correct indentation

* Replace warning svg w/ existing alert svg, and reset language strings

* fix(json): json doesn't like comma dangles

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2026-01-06 15:19:30 -06:00
Kalista Payne d87946d912 5.42.4 2025-12-19 14:11:46 -06:00
Fiz 7456ff2def Fix g1g1 notification not staying dismissed (#15580)
- use localStorage instead of sessionStorage so dismissal persists across browser sessions
- Add .stop modifier to close button click
2025-12-19 13:57:35 -06:00
Kalista Payne e0af620b40 5.42.3 2025-12-18 12:24:15 -06:00
Weblate bb295551b5 Merge branch 'origin/develop' into Weblate. 2025-12-18 19:22:25 +01:00
Kalista Payne fce400f323 Squashed commit of the following:
commit 6d4accc43d20bb89b0496cec81e3b7d2a80c51b2
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 17:00:37 2025 -0600

    fix(front): add some spacing to account for removed pieces

commit 800d8c20c9705b39438866dca360d2d2bbbbf8de
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:21:50 2025 -0600

    fix(lint): missing variable declaration

commit c16ff32d49fc11cf1fc737c242522485ecf3ef5d
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 15 17:21:00 2025 -0600

    fix(vue): markdownContainer TypeError

commit 5b4cf689b80c92e7163bcfd12b2c09f582d895f6
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 15:07:43 2025 -0600

    fix(auth): debounce password reset request
2025-12-18 10:32:08 -06:00
Weblate c0ffb8b968 Translated using Weblate (Chinese (Traditional))
Currently translated at 85.8% (237 of 276 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2446 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 76.1% (188 of 247 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (3488 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3419 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.2% (3435 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 47.3% (117 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 29.9% (74 of 247 strings)

Translated using Weblate (Polish)

Currently translated at 50.5% (1767 of 3497 strings)

Translated using Weblate (Polish)

Currently translated at 94.7% (273 of 288 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 79.0% (340 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 79.0% (340 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 84.0% (783 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 68.3% (294 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 80.7% (753 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (706 of 932 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3417 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 63.7% (176 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 75.2% (701 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 75.8% (69 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 73.1% (682 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Russian)

Currently translated at 98.6% (919 of 932 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (German)

Currently translated at 98.1% (3434 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 45.7% (113 of 247 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.7% (883 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3416 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 68.1% (62 of 91 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (3463 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Turkish)

Currently translated at 92.0% (174 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Turkish)

Currently translated at 66.3% (618 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (German)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 50.0% (138 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Turkish)

Currently translated at 62.2% (580 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (3409 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 98.9% (273 of 276 strings)

Translated using Weblate (German)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.9% (239 of 288 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.8% (2338 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.4% (2323 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.0% (707 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.4% (834 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.9% (706 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (3408 of 3497 strings)

Translated using Weblate (Italian)

Currently translated at 86.2% (163 of 189 strings)

Translated using Weblate (Italian)

Currently translated at 99.4% (927 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (275 of 276 strings)

Translated using Weblate (French)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Japanese)

Currently translated at 97.0% (3395 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 98.9% (3462 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 97.0% (3395 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.1% (3432 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.0% (845 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese)

Currently translated at 38.4% (95 of 247 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (German)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (3380 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 46.1% (114 of 247 strings)

Translated using Weblate (Dutch)

Currently translated at 95.6% (87 of 91 strings)

Translated using Weblate (French)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (French)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (274 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 98.1% (3431 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (274 of 275 strings)

Co-authored-by: Aahzmandia <aahzmandia@gmail.com>
Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Azuthrax <azuthrax@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Deleted User <noreply+908@weblate.org>
Co-authored-by: Eduardo Ariel Santos da Silva <are3380@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jakub <jakubrymuza@gmail.com>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Kaakaa <misaka.hibiki@gmail.com>
Co-authored-by: Kubo Mizuki <m.kubo.0916@gmail.com>
Co-authored-by: Kwix26 <damiandamian2133@gmail.com>
Co-authored-by: Manuel <zappyccino@proton.me>
Co-authored-by: Michael <mikosanjp@gmail.com>
Co-authored-by: Riccardo DD <emvadraen@hotmail.com>
Co-authored-by: Sam Zhong <sam.zhongzishen@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lhh <7012605@gmail.com>
Co-authored-by: nina van dijck laurijssen <n.vandijcklaurijssen@sintmartinusscholen.be>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Overview
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2025-12-18 03:50:38 +01:00
Kalista Payne 72539f9ba3 5.42.2 2025-12-10 14:16:53 -06:00
Kalista Payne dabd466719 Revert "Chat optimization (#15545)"
This reverts commit 2917955ef0.
2025-12-10 14:16:48 -06:00
Kalista Payne 8bf2304330 chore(event): G1G1 date tweaks 2025-12-10 14:15:48 -06:00
Kalista Payne 6937dc4e4e fix(subscription): couple more layout tweaks 2025-12-08 16:37:04 -06:00
Fiz 2917955ef0 Chat optimization (#15545)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown

* Show orb of rebirth confirmation modal after use (window refresh)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Fix amount of messages initially being shown

* PM_PER_PAGE set to 50

* Increases number of messages in inbox test

* Increases number of messages for inbox pagination test

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Add UUID validation for 'before' query parameter

* add party message stress test tool in admin panel

* lint

* add MAX_PM_COUNT of 400, admin tool for stress testing messages

* comment

* update stress test inbox message tool to use logged in user

* comment

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-12-05 16:12:23 -06:00
Kalista Payne 55d13e44d4 fix(subs): strings and alignments 2025-12-03 17:12:08 -06:00
Kalista Payne 90096f995f 5.42.1 2025-12-03 16:42:22 -06:00
Kalista Payne 5c74c2b914 fix(typo): gooals 2025-12-03 15:36:04 -06:00
Kalista Payne 1f1a44e16f 5.42.0 2025-11-25 19:48:43 -06:00
Weblate a275109a3e Merge branch 'origin/develop' into Weblate. 2025-11-26 02:46:05 +01:00
Weblate c65457690b Translated using Weblate (Japanese)
Currently translated at 99.6% (287 of 288 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Korean)

Currently translated at 89.1% (99 of 111 strings)

Translated using Weblate (Korean)

Currently translated at 70.5% (608 of 862 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Korean)

Currently translated at 58.0% (165 of 284 strings)

Translated using Weblate (French)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.7% (930 of 932 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (French)

Currently translated at 98.2% (847 of 862 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (French)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Spanish)

Currently translated at 98.9% (853 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (860 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.3% (926 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 96.3% (889 of 923 strings)

Translated using Weblate (Korean)

Currently translated at 86.4% (798 of 923 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.5% (241 of 247 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.7% (239 of 247 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.3% (3143 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.3% (3143 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.0% (3134 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.4% (3112 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.4% (3112 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 54.7% (1883 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.4% (3111 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 77.7% (147 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 75.6% (143 of 189 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 74.0% (140 of 189 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 71.9% (136 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.3% (3110 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 71.2% (2452 of 3441 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.7% (229 of 247 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Korean)

Currently translated at 68.0% (187 of 275 strings)

Translated using Weblate (Korean)

Currently translated at 67.2% (185 of 275 strings)

Translated using Weblate (Korean)

Currently translated at 83.2% (768 of 923 strings)

Translated using Weblate (Korean)

Currently translated at 67.2% (185 of 275 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Douglas Meneghetti <douglasrizzom@gmail.com>
Co-authored-by: Gustavo Angelo <gustavoangeloow@gmail.com>
Co-authored-by: Israel Silva Araújo <yisrael.araujo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Judes Martinez <pivet50811@limtu.com>
Co-authored-by: Lapin <sirocuro01@gmail.com>
Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de>
Co-authored-by: Leslie Munguía <moongeeuh@gmail.com>
Co-authored-by: Lio Zam <zerofux@web.de>
Co-authored-by: Raul Fogaça Sanches <raul.fsanches@hotmail.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Witza <witza87@gmail.com>
Co-authored-by: hyrge <mmsg4561@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 이승연 <opus456@naver.com>
Co-authored-by: 이영지 <oukig2036@naver.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2025-11-26 00:18:11 +01:00
Kalista Payne f740f12b97 fix(subs): missing warn text 2025-11-25 15:12:52 -06:00
Kalista Payne 9fd0bfae46 Squashed commit of the following:
commit 05933be205
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Nov 25 11:46:16 2025 -0600

    fix(layout): contrast

commit f36f3c8a21
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Nov 20 17:29:06 2025 -0600

    fix(layout): less ridiculous flow

commit 74fcbe9494
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Nov 20 17:19:23 2025 -0600

    fix(billing): adjust placement and copy

commit c0c1ea4309
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Nov 20 10:48:45 2025 -0600

    fix(groups): fine print about billing
2025-11-25 11:47:13 -06:00
Kalista Payne bee23efbef feat(content): Winter Wonderland 2025-6 2025-11-19 13:34:50 -06:00
Kalista Payne a504b18ce4 5.41.6 2025-11-06 09:28:00 -06:00
Weblate f556b102c6 Translated using Weblate (Indonesian)
Currently translated at 85.7% (162 of 189 strings)

Translated using Weblate (Indonesian)

Currently translated at 84.6% (160 of 189 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.8% (46 of 47 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Bulgarian)

Currently translated at 74.2% (124 of 167 strings)

Translated using Weblate (Bulgarian)

Currently translated at 74.2% (124 of 167 strings)

Translated using Weblate (Bulgarian)

Currently translated at 73.0% (122 of 167 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 72.8% (199 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 72.8% (199 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 69.9% (191 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 69.2% (189 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 69.2% (189 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 68.1% (186 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 67.7% (185 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 67.3% (184 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 66.3% (181 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 65.9% (180 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 65.2% (178 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 60.0% (164 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 58.9% (161 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 56.4% (154 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 55.6% (152 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 54.9% (150 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 54.9% (150 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 53.8% (147 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 53.8% (147 of 273 strings)

Translated using Weblate (French)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese)

Currently translated at 55.7% (151 of 271 strings)

Translated using Weblate (Portuguese)

Currently translated at 54.6% (148 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (269 of 284 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 17.4% (43 of 247 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Russian)

Currently translated at 76.1% (188 of 247 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (267 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 97.7% (265 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (264 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (261 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 95.5% (259 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 93.7% (254 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 91.5% (248 of 271 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.5% (259 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 71.2% (2450 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 91.1% (247 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 89.6% (243 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 87.8% (238 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.0% (267 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.3% (265 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.9% (187 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (281 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (279 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 97.8% (278 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (274 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 94.7% (269 of 284 strings)

Translated using Weblate (Russian)

Currently translated at 87.6% (249 of 284 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.6% (219 of 247 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (426 of 428 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (423 of 428 strings)

Translated using Weblate (Russian)

Currently translated at 98.3% (421 of 428 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Indonesian)

Currently translated at 80.9% (200 of 247 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Indonesian)

Currently translated at 80.9% (200 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.1% (193 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.3% (191 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.9% (190 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.2% (2347 of 3441 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.7% (227 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.7% (129 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.0% (377 of 428 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.3% (218 of 244 strings)

Translated using Weblate (Indonesian)

Currently translated at 82.5% (156 of 189 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.5% (189 of 247 strings)

Translated using Weblate (French)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Indonesian)

Currently translated at 86.8% (79 of 91 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.9% (128 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 87.3% (374 of 428 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.4% (184 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Russian)

Currently translated at 83.8% (2884 of 3441 strings)

Translated using Weblate (Korean)

Currently translated at 98.9% (191 of 193 strings)

Translated using Weblate (Korean)

Currently translated at 96.8% (187 of 193 strings)

Translated using Weblate (Korean)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Korean)

Currently translated at 98.2% (112 of 114 strings)

Translated using Weblate (Korean)

Currently translated at 50.0% (1721 of 3441 strings)

Translated using Weblate (Korean)

Currently translated at 49.9% (1720 of 3441 strings)

Translated using Weblate (Korean)

Currently translated at 49.9% (1720 of 3441 strings)

Translated using Weblate (Korean)

Currently translated at 49.8% (1717 of 3441 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (246 of 247 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Japanese)

Currently translated at 98.2% (3381 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 94.6% (125 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 67.6% (2328 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 36.0% (89 of 247 strings)

Translated using Weblate (Croatian)

Currently translated at 87.1% (115 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 75.7% (100 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 76.5% (72 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 72.3% (68 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 87.2% (213 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (278 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 74.2% (98 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Croatian)

Currently translated at 83.8% (238 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 53.8% (147 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 53.1% (145 of 273 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.0% (183 of 247 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Polish)

Currently translated at 92.0% (174 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 17.4% (43 of 247 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.7% (246 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Japanese)

Currently translated at 98.7% (244 of 247 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (921 of 923 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.7% (242 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (3378 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Japanese)

Currently translated at 97.9% (3372 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (859 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 98.5% (271 of 275 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Avoren5 <avoren@tuta.io>
Co-authored-by: Darya Ya <yatsenwork@gmail.com>
Co-authored-by: Deleted User <noreply+1516@weblate.org>
Co-authored-by: Igor <777igor93@gmail.com>
Co-authored-by: Islamiati Yulia Mustikasari Lessy <yulizee5@gmail.com>
Co-authored-by: J Y <jasmin.kf.yee@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jalojin lovlv <jalojin138@lovleo.com>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Joshua Songmin H. L <joshuasongmin@outlook.com>
Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de>
Co-authored-by: Marcus Chan <yeelok823@gmail.com>
Co-authored-by: Miroslav Denkov <habitica.exception900@passmail.net>
Co-authored-by: Raul Fogaça Sanches <raul.fsanches@hotmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: The_Blood_Orc <stefan.trbojevic188@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 지유민 <wldbals03@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/id/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/id/
Translate-URL: https://translate.habitica.com/projects/habitica/death/de/
Translate-URL: https://translate.habitica.com/projects/habitica/death/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/death/es/
Translate-URL: https://translate.habitica.com/projects/habitica/death/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/death/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/id/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/id/
Translate-URL: https://translate.habitica.com/projects/habitica/merch/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/id/
Translate-URL: https://translate.habitica.com/projects/habitica/noscript/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/id/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/id/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/id/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Merch
Translation: Habitica/Messages
Translation: Habitica/Noscript
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2025-11-05 06:25:23 +01:00
Fiz ac62de7bd8 Show orb of rebirth confirmation modal after use (window refresh) (#15540)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown

* Show orb of rebirth confirmation modal after use (window refresh)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-11-04 15:38:10 -06:00
Phillip Thelen 5ff3cc35a6 Improvements to shadow muting (#15543)
* fix test wording

* make shadow mute work for dms

* shadow mute chat messages

* shadow mute invites

* oops

* refactor mute handling into middleware

* correctly throw error

* fix

* test(chat): expect errors when muted
Also fixes the Linux version in the mongo commands. Again. wtf

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-11-04 15:35:56 -06:00
Kalista Payne 215e5e1c40 fix(reg): clarify email in use error 2025-10-30 18:23:22 -05:00
Kalista Payne 02ca96ea51 5.41.5 2025-10-23 10:08:52 -05:00
Weblate e70ae4e9aa Translated using Weblate (French)
Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (French)

Currently translated at 99.6% (3429 of 3441 strings)

Translated using Weblate (French)

Currently translated at 99.0% (3410 of 3441 strings)

Translated using Weblate (German)

Currently translated at 99.1% (245 of 247 strings)

Translated using Weblate (French)

Currently translated at 98.3% (3385 of 3441 strings)

Translated using Weblate (French)

Currently translated at 98.3% (3384 of 3441 strings)

Translated using Weblate (French)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.1% (243 of 245 strings)

Translated using Weblate (Bulgarian)

Currently translated at 72.4% (121 of 167 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Spanish)

Currently translated at 98.7% (3397 of 3441 strings)

Translated using Weblate (Spanish)

Currently translated at 97.9% (3371 of 3441 strings)

Translated using Weblate (Spanish)

Currently translated at 97.7% (3363 of 3441 strings)

Translated using Weblate (French)

Currently translated at 96.5% (3324 of 3441 strings)

Translated using Weblate (German)

Currently translated at 96.6% (3325 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3366 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (859 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (857 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 72.9% (2509 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (856 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 99.1% (855 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3366 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (854 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3366 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (270 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.8% (185 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (854 of 862 strings)

Translated using Weblate (German)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3366 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 93.6% (177 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Swedish)

Currently translated at 50.9% (140 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3365 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Swedish)

Currently translated at 85.7% (78 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Swedish)

Currently translated at 81.8% (108 of 132 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (853 of 862 strings)

Translated using Weblate (Swedish)

Currently translated at 78.0% (103 of 132 strings)

Translated using Weblate (Swedish)

Currently translated at 4.8% (12 of 245 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Russian)

Currently translated at 95.7% (181 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3364 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.0% (2412 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3362 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 98.8% (852 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 98.7% (851 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 98.4% (849 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (270 of 275 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Turkish)

Currently translated at 79.3% (150 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3361 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (German)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 97.3% (3351 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 98.2% (847 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.9% (844 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.0% (3341 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (842 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 97.5% (841 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 96.9% (836 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 72.5% (2498 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 96.8% (3332 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 96.7% (834 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 87.0% (236 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.0% (2412 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.5% (86 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.0% (2410 of 3441 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Russian)

Currently translated at 87.0% (2996 of 3441 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Russian)

Currently translated at 95.2% (180 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.9% (830 of 923 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 71.5% (2463 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.7% (242 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.3% (848 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Japanese)

Currently translated at 96.5% (3322 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 71.5% (2461 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.3% (848 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 94.9% (261 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 96.1% (3310 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 52.7% (144 of 273 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.7% (242 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 96.0% (3305 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 87.0% (236 of 271 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.1% (276 of 284 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.8% (423 of 428 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 71.4% (2460 of 3441 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 93.6% (177 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.7% (242 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.3% (848 of 862 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.4% (918 of 923 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 95.9% (3302 of 3441 strings)

Translated using Weblate (Russian)

Currently translated at 93.1% (176 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (3286 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 95.4% (3284 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 95.2% (3278 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.3% (148 of 189 strings)

Translated using Weblate (Russian)

Currently translated at 76.3% (187 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 42.4% (104 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 90.6% (781 of 862 strings)

Translated using Weblate (Dutch)

Currently translated at 99.5% (410 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 92.4% (122 of 132 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Dutch)

Currently translated at 99.2% (409 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Dutch)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Dutch)

Currently translated at 96.4% (135 of 140 strings)

Translated using Weblate (Dutch)

Currently translated at 74.5% (202 of 271 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Dutch)

Currently translated at 92.4% (122 of 132 strings)

Translated using Weblate (Dutch)

Currently translated at 76.8% (2645 of 3441 strings)

Translated using Weblate (Dutch)

Currently translated at 99.1% (242 of 244 strings)

Translated using Weblate (Dutch)

Currently translated at 42.4% (104 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Dutch)

Currently translated at 82.0% (707 of 862 strings)

Translated using Weblate (Dutch)

Currently translated at 82.4% (75 of 91 strings)

Translated using Weblate (Dutch)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Dutch)

Currently translated at 82.9% (355 of 428 strings)

Translated using Weblate (Dutch)

Currently translated at 99.1% (242 of 244 strings)

Translated using Weblate (Dutch)

Currently translated at 99.4% (188 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 76.7% (211 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 94.7% (3261 of 3441 strings)

Translated using Weblate (Dutch)

Currently translated at 96.4% (135 of 140 strings)

Translated using Weblate (Dutch)

Currently translated at 72.6% (311 of 428 strings)

Translated using Weblate (Dutch)

Currently translated at 83.0% (157 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 42.4% (104 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 72.5% (66 of 91 strings)

Translated using Weblate (Dutch)

Currently translated at 98.9% (191 of 193 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Dutch)

Currently translated at 84.1% (777 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 89.4% (169 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 81.1% (220 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 87.3% (165 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (French)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (German)

Currently translated at 99.6% (274 of 275 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (428 of 428 strings)

Translated using Weblate (Japanese)

Currently translated at 94.7% (3259 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (427 of 428 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.7% (427 of 428 strings)

Translated using Weblate (Korean)

Currently translated at 87.3% (97 of 111 strings)

Co-authored-by: Alex Dimitri <clementalexandredimitri@gmail.com>
Co-authored-by: Annie Öhlén <Yaana@users.noreply.translate.habitica.com>
Co-authored-by: Annie Öhlén <annie.ohlen@outlook.com>
Co-authored-by: Avoren5 <avoren@tuta.io>
Co-authored-by: Deleted User <noreply+1497@weblate.org>
Co-authored-by: DumbDump <Schernova13@yandex.ru>
Co-authored-by: Hoxia Lira <maghdmag@gmail.com>
Co-authored-by: Igor <777igor93@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Marcus Chan <yeelok823@gmail.com>
Co-authored-by: Niekvb <niekvb@gmail.com>
Co-authored-by: Park <changwoo0933@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tara M. Kelly <tkel6868@gmail.com>
Co-authored-by: The_Blood_Orc <stefan.trbojevic188@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Vitaliy <italik.gr@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Öz Al <dieozge2@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/content/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/nl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2025-10-23 17:08:23 +02:00
Kalista Payne e2bf8ae493 Adjustments for small screens by @Hafizzle (#15514)
* fix(responsive): adjustments for small screens by @Hafizzle

* Fixed cut off modal issues

- Fixed limited time banner being cut off on smaller devices
- Fixed Pin on purchase quest modal being cut off

---------

Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2025-10-21 15:03:34 -05:00
Kalista Payne 931a70a797 Tiny fixes (#15535)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown
2025-10-21 14:23:39 -05:00
Kalista Payne e2d2a05315 docs(migration): remove outdated comment 2025-10-14 11:17:51 -05:00
Kalista Payne be041f734d fix(lint): unsupported operator 2025-10-14 11:17:11 -05:00
Muhammad Ubaid Nawaz c430d2279c fix(migrations->users->take-this.js): Fixed processUsers function by updating query._id to match only the user's _id instead of the entire user object (#15511) 2025-10-01 13:20:43 -05:00
Kalista Payne ef592cf35f 5.41.4 2025-09-30 17:06:15 -05:00
Kalista Payne f24cd10a79 chore(subproj): update habitica-images 2025-09-30 17:06:09 -05:00
Weblate 2cd4e45016 Translated using Weblate (Croatian)
Currently translated at 100.0% (8 of 8 strings)

Merge branch 'origin/develop' into Weblate.

Translated using Weblate (Croatian)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Croatian)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 94.6% (3258 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 93.1% (54 of 58 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 83.7% (227 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 83.3% (226 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 92.7% (382 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 88.5% (365 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 82.7% (341 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 87.9% (80 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 78.0% (71 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 60.4% (55 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 43.9% (40 of 91 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.9% (3163 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.8% (3160 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Polish)

Currently translated at 34.2% (84 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.0% (267 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 86.4% (96 of 111 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.4% (245 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.6% (266 of 284 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (860 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (258 of 275 strings)

Translated using Weblate (Polish)

Currently translated at 89.0% (253 of 284 strings)

Translated using Weblate (Polish)

Currently translated at 51.7% (1780 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 94.3% (3248 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 85.9% (233 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (859 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 94.3% (3245 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.7% (255 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 94.1% (3240 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 95.3% (822 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Russian)

Currently translated at 76.3% (187 of 245 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 91.6% (394 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 94.0% (3237 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 96.6% (892 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Polish)

Currently translated at 91.5% (173 of 189 strings)

Translated using Weblate (Polish)

Currently translated at 98.5% (271 of 275 strings)

Translated using Weblate (Polish)

Currently translated at 97.6% (420 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Polish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 95.8% (412 of 430 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (French)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 93.5% (3219 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.0% (2409 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 36.3% (89 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.4% (192 of 193 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.9% (195 of 275 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

Co-authored-by: Avoren5 <avoren@tuta.io>
Co-authored-by: George <dyshlenko2@gmail.com>
Co-authored-by: Israel Silva Araújo <yisrael.araujo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jakub <jakubrymuza@gmail.com>
Co-authored-by: Jakub Rymuza <jakubrymuza@gmail.com>
Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com>
Co-authored-by: Kernis <kerhsing.wang@gmail.com>
Co-authored-by: Rafael Couto Nascimento <rafael60couto@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: The_Blood_Orc <stefan.trbojevic188@gmail.com>
Co-authored-by: Tomonari Nohki <wzpaso@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaezch <dkrovel@gmail.com>
Co-authored-by: Максим Смирнов <daseoffc@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2025-10-01 00:04:53 +02:00
Fiz 8aaff7ae23 Remove group strings from languages (besides default) (#15522)
* add new frontend files

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* add blocker to block emails from registration

* lint fixes

* Await genericPurchase completion before page reload to prevent request cancellation.

Also adds defensive check for undefined error.response in axios interceptor to prevent "t.response undefined" errors.

* Fix shop tabs overflow off screen at certain zoom levels
Fix quest cards get cut off on small screens
Fix pop-up windows extend past screen edges on mobile

* Update ToS error message

- Updated account suspension message from "This account, User ID..." to "Your account @[username] has been
  blocked..."
- Modified server auth middleware to pass username parameter when throwing account suspended error
-Modified auth utils loginRes function to include username in suspended account error
- Updated client bannedAccountModal component to pass username (empty string if unavailable)
- Updated login test to expect username in account suspended message

* lint fix

* Responsive Layout for Equipment Containers

- Added responsive CSS for mobile (<768px) and tablet (769px-1024px)
- Implemented flex-wrap layout that automatically stacks items in rows of 4 on smaller

* remove redundant disabled styles in task modals

The .disabled class conflicting with existing disabled state implementations

* Revert "Merge branch 'fiz/item-container-scaling' into qa/bat"

This reverts commit 4f28bfaad4dbeade66e507fd10904c133b206bda, reversing
changes made to 477dd6328acc6a63f5e7f1e98f2dd581665f1898.

* fix(blockers): duplicated code from rebase

* fix(admin): revert accidental change from rebase

* move !error.response to correct level

!error.response before any attempt to access error.response.status

* chore(github): split responsiveness to #15514

* Group string updates & removals

Removed the "Couple sharing tasks" and "Coworkers sharing tasks" strings:
- Removed from all language locale files
- Removed from both Vue components that used them in dropdowns:
	- groupPlanCreationModal.vue
 	- successModal.vue

* Remove some Group strings

Remove the "groupParentChildren", "groupFriends", "groupManager", and "groupTeacher" strings from all languages (except default)

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-09-30 16:46:13 -05:00
Kalista Payne 69a9fb89ef fix(staff): remove undesired parenthetical 2025-09-30 16:40:00 -05:00
Fiz e8eeb76cab Fix End challenge search to load participant based on search query (#15520)
* Load all participants when end challenge modal is opened.

* Fetch members in batches until members are loaded

* Fix challenge winner search to load all participants

Separated loading flags to prevent conflicts between modals

* Rename end challenge members flag to be more clear

* await load members

* Implement challenge member search only when searching w/debounce
2025-09-30 16:33:36 -05:00
Kalista Payne 2029739a1b chore(cg): update staff info 2025-09-25 17:43:56 -05:00
Kalista Payne 5cef106ea5 chore(analytics): remove GA by @phillipthelen 2025-09-23 17:37:10 -05:00
Kalista Payne e096d7ac42 5.41.3 2025-09-23 17:17:30 -05:00
Weblate 6db998e726 Merge branch 'origin/develop' into Weblate. 2025-09-24 00:04:32 +02:00
Kalista Payne 29c658b042 fix(faq): remove semifunctional links 2025-09-23 16:54:12 -05:00
Weblate 66710b8f38 Translated using Weblate (Japanese)
Currently translated at 93.4% (3217 of 3441 strings)

Translated using Weblate (Russian)

Currently translated at 98.3% (423 of 430 strings)

Translated using Weblate (Croatian)

Currently translated at 81.1% (220 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.3% (3211 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3206 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3205 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 97.5% (239 of 245 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.1% (238 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3204 of 3441 strings)

Co-authored-by: JohnnyDoor <kakimida.daredarone@gmail.com>
Co-authored-by: Pyak Denis Sergeevich <pyakdenis@mail.ru>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: The_Blood_Orc <stefan.trbojevic188@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/de/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translation: Habitica/Challenge
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
2025-09-23 22:30:01 +02:00
Kalista Payne c77db3d625 chore(faq): update staff list 2025-09-23 10:18:50 -05:00
Fiz c947fa97d9 Updates & Fixes: Fix Orb of Rebirth bug, update blocked player ToS message, Fix redundant disabled styling (#15494)
* add new frontend files

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* add blocker to block emails from registration

* lint fixes

* Await genericPurchase completion before page reload to prevent request cancellation.

Also adds defensive check for undefined error.response in axios interceptor to prevent "t.response undefined" errors.

* Fix shop tabs overflow off screen at certain zoom levels
Fix quest cards get cut off on small screens
Fix pop-up windows extend past screen edges on mobile

* Update ToS error message

- Updated account suspension message from "This account, User ID..." to "Your account @[username] has been
  blocked..."
- Modified server auth middleware to pass username parameter when throwing account suspended error
-Modified auth utils loginRes function to include username in suspended account error
- Updated client bannedAccountModal component to pass username (empty string if unavailable)
- Updated login test to expect username in account suspended message

* lint fix

* Responsive Layout for Equipment Containers

- Added responsive CSS for mobile (<768px) and tablet (769px-1024px)
- Implemented flex-wrap layout that automatically stacks items in rows of 4 on smaller

* remove redundant disabled styles in task modals

The .disabled class conflicting with existing disabled state implementations

* Revert "Merge branch 'fiz/item-container-scaling' into qa/bat"

This reverts commit 4f28bfaad4dbeade66e507fd10904c133b206bda, reversing
changes made to 477dd6328acc6a63f5e7f1e98f2dd581665f1898.

* fix(blockers): duplicated code from rebase

* fix(admin): revert accidental change from rebase

* move !error.response to correct level

!error.response before any attempt to access error.response.status

* chore(github): split responsiveness to #15514

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-09-22 11:12:09 -05:00
Kalista Payne b2b9702797 5.41.2 2025-09-19 16:40:15 -05:00
Weblate e92503f032 Translated using Weblate (Japanese)
Currently translated at 93.0% (3201 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (German)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (French)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (French)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (French)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (840 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 12.2% (30 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 49.6% (1707 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 86.3% (797 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 78.2% (212 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 83.8% (238 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 95.3% (184 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 65.3% (177 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 85.7% (162 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 11.4% (28 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 59.7% (162 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 71.9% (82 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 84.6% (160 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 10.2% (25 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 47.2% (128 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 71.4% (135 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 6.5% (16 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 79.9% (195 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 4.0% (10 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 70.8% (134 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 70.8% (134 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 44.6% (121 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 50.1% (137 of 273 strings)

Translated using Weblate (Japanese)

Currently translated at 93.0% (3201 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 92.9% (3200 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 95.3% (822 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 96.7% (237 of 245 strings)

Translated using Weblate (Korean)

Currently translated at 79.3% (732 of 923 strings)

Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Stefan Trbojević <stefan.trbojevic188@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 최혜연 <serpia0326@naver.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2025-09-19 23:39:32 +02:00
Fiz 8faa5b0582 Updates & Fixes: Profile Modal Tab URLs, Chat Mention Case-Insensitive, G1G1 Updates, Challenge Participants, End Challenge Modal (#15493)
* Fix profile modal tab navigation URLs for both own and other users profiles

- Add routes for /user/profile, /user/stats, and /user/achievements
- Update selectPage() to properly update URLs when switching tabs
- Own profile uses /user/{tab} format
- Other users' profiles use /profile/{userId}#{tab} format
- Parse hash fragments when navigating to other users' profile tabs
- Ensure direct navigation to tab URLs opens correct tab

* Fix undefined userId

* Server now matches usernames case insensitively like client

- Preserves original capitalization in mention text
- Fixes profile links not working with wrong case mentions

* lint fixes

* g1g1 width auto sizing w/padding

* Challenge participants spacing & text sizing fix

* Fix inconsistent profile URL format between own and other users' profiles

- Update profile tab navigation to use consistent URL format for all users
- Redirect old /user/* routes to new format for backward compatibility
- Update all navigation points (dropdown menu, notifications) to use new URLs

* Update End Challenge modal

- Replace dropdown with searchable input (384x32px) for winner selection
- Add visual badge state with gems icons for challenge completion
- Update Delete Challenge flow with refund info and proper styling
- Add close button (X) with opacity hover effect
- Enhance Award Winner button with gem icon and dynamic prize display
- Apply conditional styling based on winner selection state
- Update text colors: Maroon/50 for delete warning, Gray/100 for "OR" text
- Add proper translations for gem/gems and refund description

* lint error fixes

* end challenge modal fixes

* lint fix

* Use existing closeX component, minor UI fixes to close challenge modal

* fix lint

* Delete icon color to match text on close challenge modal

use color field to set delete icon color

* Highlight username on close challenge modal color updates

- Background color on hover: purple-600
- Text color on hover: purple-300
- Changed transition from just background-color to all so both color changes animate smoothly

* Fix strings

* Refactor g1g1 notifications from database-driven to event-based system

Changed g1g1 (gift one get one) notifications to display automatically during event periods instead of requiring database storage. Notifications now appear based on event calendar dates and use sessionStorage for dismissal state.

- Display g1g1 notification when event is active in worldState
- Store dismissal state in sessionStorage with event-specific keys
- Remove dependency on user.notifications database array
- Maintain identical user experience and appearance

* Update prize card to match participants card on challenges

* End Challenge modal UI tweaks

* Prevent false mention highlights

Prevent false mention highlights when a user's display name matches another user's username. The purple mention indicator now only appears for actual @username mentions.

* lint fixes

* Remove mention highlight

* Mention highlighting to only highlight w/username mentions

* Update G1G1 Notification

- Updated text styling for title & description
- Updated button styling
- Updated close button

* lint fix

* Add updated G1G1 notification SVGs

* Don't highlight display name w/mention

* g1g1 UI updates

- Fix sizing of gift SVGs (96px tall)
- Update button to use button element and styles <button class="btn btn-secondary mx-auto">
- Fixed positioning, color, and hover state of close icon (default white 50% opacity, hover 75% opacity)

* Fix g1g1 close icon hover state

Fix hover state of close icon (default white 50% opacity, hover 75% opacity)

* g1g1 close hover state fix

* End challenge UI updates

- Fix modal title positioning
- Fix close icon positioning
- Fix spacing between title and gem graphic
- Fix spacing between label and input field
- Fix search icon position, change input hint to "@Username"
- Set search results text align start/left with 16px starting padding.
- Fix Award Button state

* remove trailing space

* Fix exit hover state on g1g1

* fix g1g1 close icon (directly render close icon)

* new line

* Update z-index of g1g1 close button

* add display name support for mention highlighting

mention highlights now trigger for both username and display name mentions.

* Override default close button color (gray -> white)

(Also revert the renderWithMentions change)

* Fix mention display name test (& fix lint)

* Revert display name mention, strictly only username

Mentions work w/username only (works w/case insensitive as well)

* Improved case-insensitive username matching

* add close-white.svg, replace close.svg on g1g1

* find mentions that match the current user's username (case-insensitive)

* fix lint errors

* end challenge modal UI updates

* Don't change gem color on update

* disabled state button match button.scss syling

* remove padding from g1g1 close

* Directly use button.scss on end challenge modal

* Update disabled state for button.scss

* explicitly set close challenge modal button disabled/enabled state

* fix trailing space

* Add font details (and fix text color) for button disabled state

* Update award winner button min-height & padding

* button.scss button disabled styling updates

* Remove redundant disabled override on award winner button

* lint

* Use single gifts svg, and apply transform to flip horizontally

Remove unneeded gifts_end.svg

* Replaced the hardcoded #1A1B1D color with the $black from colors.scss

* Removed the 0.5em padding w/p-2

* added v-once to the refund text element

* Converted the line-height values from pixel values to multipliers
2025-09-16 22:12:41 -05:00
Kalista Payne 95494c685b 5.41.1 2025-09-16 22:05:58 -05:00
Weblate 10978d46ab Translated using Weblate (Spanish)
Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Polish)

Currently translated at 51.6% (1776 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (192 of 193 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 96.3% (236 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 36.3% (89 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 35.1% (86 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 84.0% (776 of 923 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 94.8% (818 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 92.6% (3189 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 94.6% (232 of 245 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3440 of 3441 strings)

Co-authored-by: Alexandre Le Mercier <couzinemile@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Liu leoyve <leoyve@gmail.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Shchudrov Yaroslav Maksimovich <separatationally@mail.ru>
Co-authored-by: Sven Baumann <svenbaumann1996@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Uwe B <hbtca@tunixgut.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: nagase daichi <daihachi10sub@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: インコ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2025-09-17 02:09:45 +02:00
dependabot[bot] 447eb6a0c4 chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 (#15498)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 14:58:24 -05:00
Kalista Payne 3dec49b72c GPC Message (#15508)
* feat(gpc): warn user about enabling analytics

* fix(gpc): style tweaks

* fix(privacy): local storage doesn't understand Boolean

* fix(gpc): do record if user has opted in

* fix(privacy): don't flip flop if no value changed
2025-09-11 14:58:10 -05:00
dependabot[bot] 472d03f276 chore(deps): bump vite from 6.3.5 to 6.3.6 in /website/client (#15507)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 14:53:43 -05:00
Kalista Payne fd9a27c3ab 5.41.0 2025-09-11 14:43:53 -05:00
Weblate a5c1423837 Translated using Weblate (Japanese)
Currently translated at 92.6% (3187 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (270 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.4% (229 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.1% (906 of 923 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.7% (3157 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3117 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3117 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Italian)

Currently translated at 89.6% (243 of 271 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3439 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (273 of 273 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Japanese)

Currently translated at 92.5% (3186 of 3441 strings)

Co-authored-by: Bernardo Oliveira Abrão <bernardooliveiraabrao@gmail.com>
Co-authored-by: Deleted User <noreply+1161@weblate.org>
Co-authored-by: Karictre <karictre.git@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Omar Bertolla <scaram@icloud.com>
Co-authored-by: Sven Baumann <svenbaumann1996@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: インコ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2025-09-11 21:43:25 +02:00
Phillip Thelen e9829b8b60 Phillip/admin deleter (#15466)
* refactor sending jobs to worker server

* remove unused imports

* add delete button to adminpanel

* June 2025 content build (#15437)

* chore: June 2025 content build

* chore: typo fixing

* chore: corrections to summer 2025 mage armor, spritesheet

* fix(css): rebuild spritesmith-main

---------

Co-authored-by: Kalista Payne <sabrecat@gmail.com>

* fix(script): don't use extremely costly regex

* fix(logging): don't spam empty error events

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (French)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (279 of 280 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.8% (907 of 908 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (219 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 28.1% (69 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.5% (402 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 91.5% (377 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 85.2% (774 of 908 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 63.4% (106 of 167 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 2.0% (5 of 245 strings)

Translated using Weblate (French)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Russian)

Currently translated at 64.4% (158 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.0% (837 of 862 strings)

Translated using Weblate (German)

Currently translated at 97.9% (844 of 862 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.3% (401 of 412 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (393 of 412 strings)

Translated using Weblate (Slovak)

Currently translated at 45.6% (413 of 905 strings)

Translated using Weblate (Slovak)

Currently translated at 50.8% (85 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3324 of 3325 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (429 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.6% (3114 of 3325 strings)

Translated using Weblate (Portuguese)

Currently translated at 53.9% (1793 of 3325 strings)

Translated using Weblate (Dutch)

Currently translated at 78.1% (2600 of 3325 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (242 of 243 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.6% (398 of 412 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 87.3% (2903 of 3325 strings)

Translated using Weblate (Italian)

Currently translated at 17.1% (42 of 245 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (408 of 412 strings)

Translated using Weblate (Italian)

Currently translated at 92.7% (102 of 110 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.0% (3292 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (905 of 905 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.1% (3262 of 3325 strings)

Co-authored-by: Andrea <goffopaguro@gmail.com>
Co-authored-by: Artem StolyROV <stolyarov11303@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kaya <david@kaya.sk>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Irina  Shcherbinina <cat3dcat007@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Tom <tompsognathus@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: V Aar <v.vanderaar@gmail.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Волкозмей <klippiky@gmail.com>
Co-authored-by: Данила Мальцев <maltsev-danila@inbox.ru>
Co-authored-by: Татьяна Куклева <klippiky@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/it/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent

* 5.36.4

* chore(deps): bump serialize-javascript in /website/client (#15395)

Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump axios from 1.7.4 to 1.8.2 (#15401)

Bumps [axios](https://github.com/axios/axios) from 1.7.4 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.4...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump prismjs from 1.29.0 to 1.30.0 (#15403)

Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/runtime-corejs2 in /website/client (#15406)

Bumps [@babel/runtime-corejs2](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs2) from 7.23.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime-corejs2)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs2"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/helpers in /website/client (#15407)

Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.23.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/runtime from 7.23.9 to 7.26.10 (#15410)

Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.23.9 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump http-proxy-middleware in /website/client (#15427)

Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Optimize database access for some use cases (#15444)

* optimize query when listing challenge tasks

* Optimize query for checking if user is party leader

* correct worker call

* remove unused priority

* fix tests

* don’t use body with delete

* add detailed information about sub payment for google and apple

* Support paypal details for subscription in admin panel

* stripe payment details

* fix imports

* fix tests

* fix deleting account

* begin building group admin panel

* fix convertig sub to group plan

* improve sub status display

* fix lint

* fix long line

* fix sub state display

* lint fix

* fix

* delete amplitude data by default

* improve searching for email in admin panel

* correctly call method

* move delete button in admin panel

* fix(lint): whitespace

* fix(style): indent

* fix(typo): humand

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Andrea <goffopaguro@gmail.com>
Co-authored-by: Artem StolyROV <stolyarov11303@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kaya <david@kaya.sk>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Irina  Shcherbinina <cat3dcat007@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Tom <tompsognathus@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: V Aar <v.vanderaar@gmail.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Волкозмей <klippiky@gmail.com>
Co-authored-by: Данила Мальцев <maltsev-danila@inbox.ru>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-09-09 16:41:03 -05:00
592 changed files with 20401 additions and 10121 deletions
+18 -8
View File
@@ -82,7 +82,7 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:sanity
common:
runs-on: ubuntu-latest
strategy:
@@ -129,13 +129,13 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:content
api-unit:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -144,11 +144,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -158,15 +160,17 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api:unit
env:
REQUIRES_SERVER=true: true
api-v3-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -176,10 +180,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -189,15 +194,18 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v3:integration
env:
REQUIRES_SERVER=true: true
api-v4-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -207,10 +215,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -220,6 +229,7 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v4:integration
env:
REQUIRES_SERVER=true: true
+1 -1
View File
@@ -47,5 +47,5 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data*
/mongodb-*
/.nyc_output
+2 -2
View File
@@ -46,7 +46,7 @@
"MAINTENANCE_MODE": "false",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs&directConnection=true&readPreference=secondary",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
@@ -90,7 +90,7 @@
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs&directConnection=true&readPreference=secondary",
"TIME_TRAVEL_ENABLED": "false",
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"WEB_CONCURRENCY": 1
-53
View File
@@ -1,53 +0,0 @@
services:
client:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev"]
depends_on:
- server
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "8080:8080"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: mongo:5.0.23
restart: unless-stopped
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
networks:
- habitica
ports:
- "27017:27017"
networks:
habitica:
driver: bridge
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-only"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-test"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker-testing:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+43 -22
View File
@@ -1,35 +1,56 @@
version: "3"
services:
client:
build: .
networks:
- habitica
environment:
- BASE_URL=http://server:3000
ports:
- "8080:8080"
command: ["npm", "run", "client:dev"]
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev:docker"]
depends_on:
- server
server:
build: .
ports:
- "3000:3000"
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "5173:5173"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
depends_on:
- mongo
mongo:
image: mongo:3.6
ports:
- "27017:27017"
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: "mongo:7.0"
container_name: "habitica-mongodb"
networks:
- habitica
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
networks:
habitica:
+12 -9
View File
@@ -5,7 +5,7 @@ import path from 'path';
import babel from 'gulp-babel';
import os from 'os';
import fs from 'fs';
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
import spawn from 'cross-spawn';
import clean from 'rimraf';
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
@@ -35,7 +35,7 @@ gulp.task('build:prod', gulp.series(
// When used on windows `run-rs` must first be run without the `--keep` option
// in order to be setup correctly, afterwards it can be used.
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
const MONGO_PATH = path.join(__dirname, '/../mongodb-data-docker/');
gulp.task('build:prepare-mongo', async () => {
if (fs.existsSync(MONGO_PATH)) {
@@ -51,29 +51,32 @@ gulp.task('build:prepare-mongo', async () => {
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
// use run-rs without --keep, kill it as soon as the replica set starts
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
const dockerMongoProcess = spawn('npm', ['run', 'docker:mongo:dev']);
for await (const chunk of runRsProcess.stdout) {
let manuallyStopped = false;
for await (const chunk of dockerMongoProcess.stdout) {
const stringChunk = chunk.toString();
console.log(stringChunk); // eslint-disable-line no-console
// kills the process after the replica set is setup
if (stringChunk.includes('Started replica set')) {
if (stringChunk.includes('mongod startup complete')) {
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
runRsProcess.kill();
dockerMongoProcess.kill();
manuallyStopped = true;
}
}
let error = '';
for await (const chunk of runRsProcess.stderr) {
for await (const chunk of dockerMongoProcess.stderr) {
const stringChunk = chunk.toString();
error += stringChunk;
}
const exitCode = await new Promise(resolve => {
runRsProcess.on('close', resolve);
dockerMongoProcess.on('close', resolve);
});
if (exitCode || error.length > 0) {
if (!manuallyStopped && (exitCode || error.length > 0)) {
// remove any leftover files
clean.sync(MONGO_PATH);
+61 -26
View File
@@ -6,9 +6,21 @@ gulp.task('cache:content', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
const {
CONTENT_CACHE_PATH,
getLocalizedContentResponse,
IOS_FILTER,
ANDROID_FILTER,
buildFilterObject,
hashForFilter,
} = require('../website/server/libs/content'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const iosHash = hashForFilter(IOS_FILTER);
const iosFilterObj = buildFilterObject(IOS_FILTER);
const androidHash = hashForFilter(ANDROID_FILTER);
const androidFilterObj = buildFilterObject(ANDROID_FILTER);
try {
// create the cache folder (if it doesn't exist)
try {
@@ -26,33 +38,56 @@ gulp.task('cache:content', done => {
getLocalizedContentResponse(langCode),
'utf8',
);
});
done();
} catch (err) {
done(err);
}
});
gulp.task('cache:i18n', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { BROWSER_SCRIPT_CACHE_PATH, geti18nBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
try {
// create the cache folder (if it doesn't exist)
try {
fs.mkdirSync(BROWSER_SCRIPT_CACHE_PATH);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
// create and save the i18n browser script for each language
langCodes.forEach(languageCode => {
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}${languageCode}.js`,
geti18nBrowserScript(languageCode),
`${CONTENT_CACHE_PATH}${langCode}${iosHash}.json`,
getLocalizedContentResponse(langCode, iosFilterObj),
'utf8',
);
fs.writeFileSync(
`${CONTENT_CACHE_PATH}${langCode}${androidHash}.json`,
getLocalizedContentResponse(langCode, androidFilterObj),
'utf8',
);
});
done();
} catch (err) {
done(err);
}
});
function safeMkdir (path) {
try {
fs.mkdirSync(path);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
}
gulp.task('cache:i18n', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { BROWSER_SCRIPT_CACHE_PATH, geti18nCoreBrowserScript, geti18nContentBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
try {
// create the cache folders (if they doesn't exist)
safeMkdir(BROWSER_SCRIPT_CACHE_PATH);
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}core/`);
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}content/`);
// create and save the i18n browser script for each language
langCodes.forEach(languageCode => {
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}core/${languageCode}.js`,
geti18nCoreBrowserScript(languageCode),
'utf8',
);
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}content/${languageCode}.js`,
geti18nContentBrowserScript(languageCode),
'utf8',
);
});
+5
View File
@@ -53,6 +53,11 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
console.info({
mongooseOptions,
connectionUrl,
});
mongoose.connect(connectionUrl, mongooseOptions)
.then(() => mongoose.connection.dropDatabase())
.then(() => mongoose.connection.close()).then(() => {
+1 -1
View File
@@ -73,7 +73,7 @@ export default async function processUsers () {
break;
} else {
query._id = {
$gt: users[users.length - 1],
$gt: users[users.length - 1]._id,
};
}
+493 -637
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -1,13 +1,12 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.40.2",
"version": "5.44.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@google-analytics/data": "^4.12.1",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
@@ -41,6 +40,7 @@
"gulp-imagemin": "^7.1.0",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
@@ -49,10 +49,12 @@
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"on-headers": "^1.1.0",
@@ -101,13 +103,16 @@
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:dev:docker": "cd website/client && npm run serve:docker",
"client:build": "cd website/client && npm run build",
"client:unit": "cd website/client && npm run test:unit",
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "node --watch --inspect ./website/server/index.js",
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2204 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"docker:aio": "docker compose -f docker-compose.yml up",
"docker:mongo:dev": "docker compose -f docker-compose.mongo-only.yml up",
"docker:mongo:dev:down": "docker compose -f docker-compose.mongo-only.yml down",
"docker:mongo:test": "docker compose -f docker-compose.mongo-test-local.yml up",
"mongo:test": "node scripts/start-local-mongo.mjs --test-db",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
@@ -123,7 +128,6 @@
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
+3 -3
View File
@@ -150,7 +150,7 @@ describe('emails', () => {
sendTxn(mailingInfo, emailType);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
@@ -234,7 +234,7 @@ describe('emails', () => {
sendTxn(mailingInfo, emailType);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
@@ -254,7 +254,7 @@ describe('emails', () => {
sendTxn(mailingInfo, emailType, variables);
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
json: {
data: {
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
});
it('highlights users with case-insensitive matching', async () => {
const text = '@USER: message @User2 @USER3';
const result = await highlightMentions(text);
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
});
it('doesn\'t highlight nonexisting users', async () => {
const text = '@nouser message';
const result = await highlightMentions(text);
+58 -38
View File
@@ -12,11 +12,33 @@ const { i18n } = common;
describe('Apple Payments', () => {
const subKey = 'basic_3mo';
let iapSetupStub;
let iapValidateStub;
let iapIsValidatedStub;
let iapIsCanceledStub;
let iapIsExpiredStub;
let paymentBuySkuStub;
let iapGetPurchaseDataStub;
let validateGiftMessageStub;
let paymentsCreateSubscritionStub;
beforeEach(() => {
iapSetupStub = sinon.stub(iap, 'setup').resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
});
describe('verifyPurchase', () => {
let sku; let user; let token; let receipt; let
headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
iapGetPurchaseDataStub; let validateGiftMessageStub;
beforeEach(() => {
token = 'testToken';
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
receipt = `{"token": "${token}", "productId": "${sku}"}`;
headers = {};
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isCanceled').returns(false);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
productId: 'com.habitrpg.ios.Habitica.21gems',
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
describe('subscribe', () => {
let sub; let sku; let user; let token; let receipt; let headers; let
nextPaymentProcessing;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
let paymentsCreateSubscritionStub; let
iapGetPurchaseDataStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
nextPaymentProcessing = moment.utc().add({ days: 2 });
user = new User();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
});
});
it('should throw an error if no active subscription is found', async () => {
iap.isCanceled.restore();
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
.returns(true);
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: -2 }).toDate(),
purchaseDate: new Date(),
productId: 'subscription1month',
transactionId: token,
originalTransactionId: token,
}]);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
const subOptions = [
{
sku: 'subscription1month',
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let headers; let customerId; let
expirationDate;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
let paymentCancelSubscriptionSpy;
beforeEach(async () => {
token = 'test-token';
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
customerId = 'test-customerId';
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub.restore();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({
expirationDate,
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(true);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
paymentCancelSubscriptionSpy.restore();
});
it('should throw an error if we are missing a subscription', async () => {
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapIsCanceledStub).to.be.calledOnce;
expect(iapIsExpiredStub).to.be.calledOnce;
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
+53 -39
View File
@@ -11,12 +11,36 @@ const { i18n } = common;
describe('Google Payments', () => {
const subKey = 'basic_3mo';
let iapSetupStub;
let iapValidateStub;
let iapIsValidatedStub;
let paymentBuySkuStub;
let validateGiftMessageStub;
beforeEach(() => {
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(false);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isCanceled.restore();
iap.isExpired.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
describe('verifyPurchase', () => {
let sku; let user; let token; let receipt; let signature; let
headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentBuySkuStub; let validateGiftMessageStub;
beforeEach(() => {
sku = 'com.habitrpg.android.habitica.iap.21gems';
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
signature = '';
headers = {};
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
it('should throw an error if receipt is invalid', async () => {
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
describe('subscribe', () => {
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
nextPaymentProcessing;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentsCreateSubscritionStub;
let paymentsCreateSubscritionStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
signature = '';
nextPaymentProcessing = moment.utc().add({ days: 2 });
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
payments.createSubscription.restore();
});
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let signature; let headers; let customerId; let
expirationDate;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
beforeEach(async () => {
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
signature = '';
customerId = 'test-customerId';
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
user = new User();
user.profile.name = 'sender';
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
});
it('should cancel a user subscription', async () => {
iap.isCanceled.restore();
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
});
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
iap.isCanceled.restore();
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
const laterDate = moment.utc().add(7, 'days');
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate, autoRenewing: false },
{ expirationDate: laterDate, autoRenewing: false },
.returns([{
startTimeMillis: expirationDate.valueOf(),
expirationDate,
autoRenewing: false,
}, {
startTimeMillis: laterDate.valueOf(),
expirationDate: laterDate,
autoRenewing: false,
},
]);
await googlePayments.cancelSubscribe(user, headers);
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ autoRenewing: true }]);
await googlePayments.cancelSubscribe(user, headers);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
.returns([{ expirationDate, autoRenewing: false },
{ autoRenewing: true },
{ expirationDate, autoRenewing: false }]);
await googlePayments.cancelSubscribe(user, headers);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
+31 -13
View File
@@ -9,7 +9,7 @@ import {
import {
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
} from '../../../../../website/server/models/group';
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import { MAX_MESSAGE_LENGTH, CHAT_FLAG_FROM_SHADOW_MUTE } from '../../../../../website/common/script/constants';
import * as email from '../../../../../website/server/libs/email';
describe('POST /chat', () => {
@@ -80,17 +80,20 @@ describe('POST /chat', () => {
member.updateOne({ 'flags.chatRevoked': false });
});
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
it('errors when chat privileges are revoked when sending a message to a private guild', async () => {
await member.updateOne({
'flags.chatRevoked': true,
});
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
await expect(member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage }))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('does not error when chat privileges are revoked when sending a message to a party', async () => {
it('errors when chat privileges are revoked when sending a message to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
@@ -106,9 +109,12 @@ describe('POST /chat', () => {
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
await expect(privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage }))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
@@ -123,7 +129,7 @@ describe('POST /chat', () => {
member.updateOne({ 'flags.chatShadowMuted': false });
});
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
it('creates a chat with flagCount set when sending a message to a private guild', async () => {
await member.updateOne({
'flags.chatShadowMuted': true,
});
@@ -131,10 +137,10 @@ describe('POST /chat', () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
});
it('creates a chat with zero flagCount when sending a message to a party', async () => {
it('creates a chat with flagCount set when sending a message to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
@@ -153,7 +159,7 @@ describe('POST /chat', () => {
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
});
});
@@ -238,6 +244,18 @@ describe('POST /chat', () => {
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with case-insensitive mentions', async () => {
const originalUsername = member.auth.local.username;
const uppercaseUsername = originalUsername.toUpperCase();
const messageWithMentions = `hi @${uppercaseUsername}`;
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with a max length of 3000 chars', async () => {
const veryLongMessage = `
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.
@@ -61,6 +61,24 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('fakes sending an invite if user is shadow muted', async () => {
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
const userToInvite = await generateUser();
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername],
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user'))
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
});
it('invites a user to a group by username', async () => {
const userToInvite = await generateUser();
@@ -209,6 +227,24 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('fakes sending an invite if user is shadow muted', async () => {
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
const userToInvite = await generateUser();
const response = await inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user'))
.to.eventually.not.have.nested.property('invitations.parties[0].id', group._id);
});
it('invites a user to a group by uuid', async () => {
const userToInvite = await generateUser();
@@ -281,6 +317,19 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('fakes sending invite when inviter is shadow muted', async () => {
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
const res = await inviterMuted.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
});
const updatedUser = await inviterMuted.sync();
expect(res).to.exist;
expect(updatedUser.invitesSent).to.eql(1);
});
it('returns an error when invite is missing an email', async () => {
await expect(inviter.post(`/groups/${group._id}/invite`, {
emails: [{ name: 'test' }],
@@ -405,6 +454,19 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('fakes sending an invite if user is shadow muted', async () => {
const inviterMuted = await inviter.updateOne({ 'flags.chatShadowMuted': true });
const newUser = await generateUser();
const invite = await inviterMuted.post(`/groups/${group._id}/invite`, {
uuids: [newUser._id],
emails: [{ name: 'test', email: 'test@habitica.com' }],
});
const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.parties[0]).to.not.exist;
expect(invite).to.exist;
});
it('invites users to a group by uuid and email', async () => {
const newUser = await generateUser();
const invite = await inviter.post(`/groups/${group._id}/invite`, {
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
it('returns four messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i += 1) {
for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
@@ -43,7 +43,7 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns error when to user has blocked the sender', async () => {
it('returns error when recipient has blocked the sender', async () => {
const receiver = await generateUser({ 'inbox.blocks': [userToSendMessage._id] });
await expect(userToSendMessage.post('/members/send-private-message', {
@@ -56,7 +56,7 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns error when sender has blocked to user', async () => {
it('returns error when sender has blocked recipient', async () => {
const receiver = await generateUser();
const sender = await generateUser({ 'inbox.blocks': [receiver._id] });
@@ -70,7 +70,7 @@ describe('POST /members/send-private-message', () => {
});
});
it('returns error when to user has opted out of messaging', async () => {
it('returns error when recipient has opted out of messaging', async () => {
const receiver = await generateUser({ 'inbox.optOut': true });
await expect(userToSendMessage.post('/members/send-private-message', {
@@ -174,7 +174,7 @@ describe('POST /members/send-private-message', () => {
expect(notification.data.excerpt).to.equal(messageExcerpt);
});
it('allows admin to send when sender has blocked the admin', async () => {
it('allows admin to send when recipient has blocked the admin', async () => {
userToSendMessage = await generateUser({
'permissions.moderator': true,
});
@@ -202,7 +202,7 @@ describe('POST /members/send-private-message', () => {
expect(sendersMessageInSendersInbox).to.exist;
});
it('allows admin to send when to user has opted out of messaging', async () => {
it('allows admin to send when recipient has opted out of messaging', async () => {
userToSendMessage = await generateUser({
'permissions.moderator': true,
});
@@ -229,4 +229,58 @@ describe('POST /members/send-private-message', () => {
expect(sendersMessageInReceiversInbox).to.exist;
expect(sendersMessageInSendersInbox).to.exist;
});
describe('sender is shadow muted', () => {
beforeEach(async () => {
userToSendMessage = await generateUser({
'flags.chatShadowMuted': true,
});
});
it('does not save the message in the receiver inbox', async () => {
const receiver = await generateUser();
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
expect(response.message.uuid).to.equal(receiver._id);
const updatedReceiver = await receiver.get('/user');
const updatedSender = await userToSendMessage.get('/user');
const sendersMessageInReceiversInbox = _.find(
updatedReceiver.inbox.messages,
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
);
const sendersMessageInSendersInbox = _.find(
updatedSender.inbox.messages,
message => message.uuid === receiver._id && message.text === messageToSend,
);
expect(sendersMessageInReceiversInbox).to.not.exist;
expect(sendersMessageInSendersInbox).to.exist;
});
it('does not save the message message twice if recipient is sender', async () => {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: userToSendMessage._id,
});
expect(response.message.uuid).to.equal(userToSendMessage._id);
const updatedSender = await userToSendMessage.get('/user');
const sendersMessageInSendersInbox = _.find(
updatedSender.inbox.messages,
message => message.uuid === userToSendMessage._id && message.text === messageToSend,
);
expect(sendersMessageInSendersInbox).to.exist;
expect(Object.keys(updatedSender.inbox.messages).length).to.equal(1);
});
});
});
@@ -44,7 +44,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id, username: user.auth.local.username }),
});
});
@@ -65,6 +65,52 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Google User Name',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername');
});
it('generates a random username if provided username contains only disallowed characters', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Áîüè',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('generates a random username if provided username contains a disallowed word', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'i am a TESTPLACEHOLDERSLURWORDHERE',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('generates a random username if sanitized username conflicts with an extant user', async () => {
user = await generateUser({ 'auth.local.username': 'GoogleUserName' });
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Google User Name',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('fails if allowRegister is false and user does not exist', async () => {
await expect(api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
it('returns five messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i += 1) {
for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
+9 -1
View File
@@ -20,6 +20,9 @@ describe('shared.ops.unlock', () => {
beforeEach(() => {
user = generateUser();
user.balance = usersStartingGems;
user.pinnedItems.push({ type: 'background', path: 'backgrounds.backgrounds042016.giant_florals' });
user.pinnedItems.push({ type: 'haircolor', path: 'hair.color.rainbow' });
user.pinnedItems.push({ type: 'shirt', path: 'shirt.convict' });
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
});
@@ -272,6 +275,7 @@ describe('shared.ops.unlock', () => {
});
it('unlocks an item (appearance)', async () => {
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.not.equal(-1);
const path = unlockPath.split(',')[0];
const initialShirts = Object.keys(user.purchased.shirt).length;
const [, message] = await unlock(user, { query: { path } });
@@ -282,11 +286,12 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.equal(-1);
});
it('unlocks an item (hair color)', async () => {
user.purchased.hair.color = {};
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.not.equal(-1);
const path = hairUnlockPath.split(',')[0];
const initialColorHair = Object.keys(user.purchased.hair.color).length;
const [, message] = await unlock(user, { query: { path } });
@@ -297,6 +302,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.equal(-1);
});
it('unlocks an item (facial hair)', async () => {
@@ -334,6 +340,7 @@ describe('shared.ops.unlock', () => {
it('unlocks an item (background)', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.not.equal(-1);
const [, message] = await unlock(user, {
query: { path: backgroundUnlockPath },
});
@@ -344,6 +351,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 1.75);
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.equal(-1);
});
it('handles an invalid hair path gracefully', async () => {
+1 -6
View File
@@ -1,12 +1,7 @@
import { STRING_ERROR_MSG, STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
import { STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
import translator from '../../website/common/script/content/translation';
describe('Translator', () => {
it('returns error message if string is not properly formatted', () => {
const improperlyFormattedString = translator('petName', { attr: 0 })();
expect(improperlyFormattedString).to.match(STRING_ERROR_MSG);
});
it('returns an error message if string does not exist', () => {
const stringDoesNotExist = translator('stringDoesNotExist')();
expect(stringDoesNotExist).to.match(STRING_DOES_NOT_EXIST_MSG);
+2 -2
View File
@@ -1,8 +1,8 @@
import i18n from '../../website/common/script/i18n';
import './globals.helper';
import { translations } from '../../website/server/libs/i18n';
import { contentTranslations } from '../../website/server/libs/i18n';
i18n.translations = translations;
i18n.translations = contentTranslations;
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
+1
View File
@@ -21,6 +21,7 @@ export async function getProperty (collectionName, id, path) {
// Specifically helpful for the GET /groups tests,
// resets the db to an empty state and creates a tavern document
export async function resetHabiticaDB () {
console.info('Resetting Habitica DB');
const groups = mongoose.connection.db.collection('groups');
const users = mongoose.connection.db.collection('users');
return mongoose.connection.dropDatabase()
+1 -1
View File
@@ -32,6 +32,6 @@
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
</body>
</html>
+4980 -2714
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"serve": "vite",
"serve:docker": "npx vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run",
@@ -27,13 +28,13 @@
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
"eslint-plugin-vue": "7.20.0",
"ga-gtag": "^1.2.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"markdown-it": "^14.0.0",
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"nconf": "^0.12.1",
"sass": "^1.63.4",
@@ -42,7 +43,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
+3
View File
@@ -203,6 +203,9 @@ 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
@@ -1055,6 +1055,11 @@
width: 141px;
height: 147px;
}
.background_elegant_palace {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elegant_palace.png');
width: 141px;
height: 147px;
}
.background_enchanted_music_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
width: 141px;
@@ -1751,6 +1756,11 @@
width: 141px;
height: 147px;
}
.background_nighttime_street_with_shops {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_nighttime_street_with_shops.png');
width: 141px;
height: 147px;
}
.background_ocean_sunrise {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_ocean_sunrise.png');
width: 141px;
@@ -2437,6 +2447,11 @@
width: 141px;
height: 147px;
}
.background_winter_desert_with_saguaros {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_desert_with_saguaros.png');
width: 141px;
height: 147px;
}
.background_winter_fireworks {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_winter_fireworks.png');
width: 141px;
@@ -29455,6 +29470,11 @@
width: 60px;
height: 60px;
}
.back_armoire_harpsichord {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_armoire_harpsichord.png');
width: 114px;
height: 90px;
}
.body_armoire_clownsBowtie {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/body_armoire_clownsBowtie.png');
width: 114px;
@@ -29845,6 +29865,11 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_loneCowpokeOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_loneCowpokeOutfit.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_lunarArmor {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lunarArmor.png');
width: 90px;
@@ -30480,6 +30505,11 @@
width: 114px;
height: 87px;
}
.head_armoire_loneCowpokeHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_loneCowpokeHat.png');
width: 114px;
height: 90px;
}
.head_armoire_lunarCrown {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lunarCrown.png');
width: 90px;
@@ -30790,6 +30820,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_doubleBass {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_doubleBass.png');
width: 114px;
height: 90px;
}
.shield_armoire_dragonTamerShield {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dragonTamerShield.png');
width: 90px;
@@ -30995,6 +31030,11 @@
width: 90px;
height: 90px;
}
.shield_armoire_prettyPinkGiftBox {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_prettyPinkGiftBox.png');
width: 114px;
height: 90px;
}
.shield_armoire_ramHornShield {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_ramHornShield.png');
width: 90px;
@@ -31465,6 +31505,11 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_loneCowpokeOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_loneCowpokeOutfit.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_lunarArmor {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lunarArmor.png');
width: 90px;
@@ -31760,6 +31805,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_bambooFlute {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_bambooFlute.png');
width: 114px;
height: 90px;
}
.weapon_armoire_barristerGavel {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_barristerGavel.png');
width: 90px;
@@ -32170,6 +32220,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_prettyPinkParasol {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_prettyPinkParasol.png');
width: 114px;
height: 90px;
}
.weapon_armoire_pushBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
width: 114px;
@@ -34060,6 +34115,46 @@
width: 90px;
height: 90px;
}
.back_mystery_202601 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202601.png');
width: 114px;
height: 90px;
}
.back_mystery_202602 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202602.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png');
width: 114px;
height: 90px;
}
.head_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
width: 114px;
height: 90px;
}
.head_mystery_202602 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202602.png');
width: 114px;
height: 90px;
}
.slim_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
width: 114px;
height: 90px;
}
.weapon_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png');
width: 114px;
height: 90px;
}
.weapon_mystery_202601 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202601.png');
width: 114px;
height: 90px;
}
.back_mystery_201402 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
width: 90px;
@@ -38640,6 +38735,26 @@
width: 114px;
height: 90px;
}
.broad_armor_special_winter2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_winter2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Mage.png');
width: 114px;
height: 90px;
}
.broad_armor_special_winter2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Rogue.png');
width: 117px;
height: 120px;
}
.broad_armor_special_winter2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_winter2026Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_yeti {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_yeti.png');
width: 90px;
@@ -38935,6 +39050,26 @@
width: 114px;
height: 90px;
}
.head_special_winter2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Healer.png');
width: 114px;
height: 90px;
}
.head_special_winter2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Mage.png');
width: 114px;
height: 90px;
}
.head_special_winter2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Rogue.png');
width: 117px;
height: 120px;
}
.head_special_winter2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_winter2026Warrior.png');
width: 114px;
height: 90px;
}
.head_special_yeti {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_yeti.png');
width: 90px;
@@ -39115,6 +39250,21 @@
width: 114px;
height: 90px;
}
.shield_special_winter2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Healer.png');
width: 114px;
height: 90px;
}
.shield_special_winter2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Rogue.png');
width: 117px;
height: 120px;
}
.shield_special_winter2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_winter2026Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_yeti {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_yeti.png');
width: 90px;
@@ -39355,6 +39505,26 @@
width: 114px;
height: 90px;
}
.slim_armor_special_winter2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_winter2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Mage.png');
width: 114px;
height: 90px;
}
.slim_armor_special_winter2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Rogue.png');
width: 117px;
height: 120px;
}
.slim_armor_special_winter2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_winter2026Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_yeti {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_yeti.png');
width: 90px;
@@ -39595,6 +39765,26 @@
width: 114px;
height: 90px;
}
.weapon_special_winter2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_winter2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Mage.png');
width: 114px;
height: 90px;
}
.weapon_special_winter2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Rogue.png');
width: 117px;
height: 120px;
}
.weapon_special_winter2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_winter2026Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_yeti {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_yeti.png');
width: 90px;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,9 @@
<svg width="378" height="176" viewBox="0 0 378 176" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0H378V174C378 175.105 377.105 176 376 176H1.99999C0.895423 176 0 175.105 0 174V0Z" fill="url(#paint0_linear_2257_239)"/>
<defs>
<linearGradient id="paint0_linear_2257_239" x1="378" y1="0" x2="0" y2="0" gradientUnits="userSpaceOnUse">
<stop stop-color="#925CF3"/>
<stop offset="1" stop-color="#34B5C1"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 448 B

@@ -0,0 +1,37 @@
<svg width="48" height="96" viewBox="0 0 48 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.10104 12.0483C-2.82088 9.43721 -3.53422 6.57214 -5.6115 5.24584C-7.68877 3.91954 -9.89543 4.92709 -10.1422 6.808C-10.3891 8.68891 -9.06061 9.83066 -4.97737 13.9337C-3.81821 15.0985 -3.3812 14.6594 -3.10104 12.0483Z" stroke="#FFA624" stroke-width="4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.34089 15.2054C4.45116 13.6561 7.27707 12.8443 9.45877 13.9889C11.6405 15.1334 11.8754 17.5575 10.3778 18.7127C8.88016 19.868 7.23193 19.2828 1.65411 17.781C0.0706697 17.3546 0.230624 16.7548 2.34089 15.2054Z" stroke="#FFBE5D" stroke-width="4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.549002 12.0098C-3.61871 9.59194 -3.87667 15.8322 -2.20457 16.8023C-0.532473 17.7724 4.71671 14.4277 0.549002 12.0098Z" fill="#EE9109"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L13.637 24.9825L9.18965 32.7229L-6.21656 23.785L-1.76917 16.0445Z" fill="#F8F9F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.90457 13.0652L3.36623 19.0238L-1.08116 26.7643L-11.352 20.8057L-6.90457 13.0652Z" fill="#FFBE5D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-1.76917 16.0445L3.36623 19.0238L1.88377 21.604L-3.25163 18.6247L-1.76917 16.0445Z" fill="#FFA624"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-6.21656 23.785L6.62195 31.2333L-3.75529 49.2944L-16.5938 41.8461L-6.21656 23.785Z" fill="#F8F9F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.64886 25.2747L6.62195 31.2333L5.13948 33.8134L-5.13132 27.8548L-3.64886 25.2747Z" fill="#DDF3F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.401307 24.1842L10.6721 30.1428L9.18965 32.7229L-1.08116 26.7643L0.401307 24.1842Z" fill="#DDF3F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.7924 38.4607L17.9387 42.0519L21.31 40.5834L24.8838 41.4413L23.4225 38.0537L24.2762 34.4625L20.9049 35.9309L17.3311 35.0731L18.7924 38.4607Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M-3.93867 71.2331L-4.79238 74.8243L-1.42111 73.3559L2.15271 74.2137L0.691383 70.8261L1.54509 67.2349L-1.82618 68.7033L-5.4 67.8455L-3.93867 71.2331Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.8949 25.3807L35.0583 29.8802L37.9424 26.2452L42.4202 25.0761L38.8028 22.178L37.6393 17.6786L34.7552 21.3135L30.2775 22.4826L33.8949 25.3807Z" fill="white" fill-opacity="0.5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L40.579 68.1435L45.9507 88.2881L31.6312 92.1436L26.2596 71.999Z" fill="#F8F9F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L26.2589 71.9966L31.6273 92.1421L17.3084 96L11.9401 75.8545Z" fill="#DDF3F3"/>
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 23.3957 72.7701)" fill="#FFA624"/>
<rect width="2.96589" height="20.8485" transform="matrix(0.965611 -0.25999 0.257652 0.966238 26.2596 71.999)" fill="#FFBE5D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.9999 90.0369L30.8638 89.2658L31.6312 92.1436L28.7673 92.9147L27.9999 90.0369Z" fill="#EE9109"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3957 72.7701L26.2596 71.999L27.0269 74.8768L24.163 75.6479L23.3957 72.7701Z" fill="#EE9109"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9401 75.8545L23.3951 72.7682L24.162 75.6461L12.707 78.7325L11.9401 75.8545Z" fill="#C1E9E9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.5443 93.1213L27.9999 90.0369L28.7673 92.9147L17.3117 95.9991L16.5443 93.1213Z" fill="#C1E9E9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.1235 71.2279L40.579 68.1435L41.3464 71.0213L29.8908 74.1057L29.1235 71.2279Z" fill="#DDF3F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.7277 88.4947L45.1833 85.4103L45.9507 88.2881L34.4951 91.3725L33.7277 88.4947Z" fill="#DDF3F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.8638 89.2658L33.7277 88.4947L34.4951 91.3725L31.6312 92.1436L30.8638 89.2658Z" fill="#FFA624"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.2596 71.999L29.1235 71.2279L29.8908 74.1057L27.0269 74.8768L26.2596 71.999Z" fill="#FFA624"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.5224 56.3076C25.8087 53.7812 24.0792 51.3933 21.6588 50.9455C19.2383 50.4977 17.5679 52.2625 18.0403 54.0994C18.5126 55.9363 20.17 56.4948 25.4855 58.7621C26.9945 59.4057 27.236 58.834 26.5224 56.3076Z" stroke="#FFA624" stroke-width="4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.745 57.1864C34.124 54.9555 36.4415 53.1391 38.8911 53.3791C41.3406 53.6191 42.4621 55.7782 41.5042 57.413C40.5463 59.0479 38.7999 59.1258 33.0684 59.8329C31.4413 60.0337 31.366 59.4173 32.745 57.1864Z" stroke="#FFBE5D" stroke-width="4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.8923 54.898C25.1267 54.225 27.2139 60.108 29.1258 60.378C31.0378 60.648 34.6579 55.571 29.8923 54.898Z" fill="#EE9109"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L46.8635 61.9994L45.6255 70.8503L28.0091 68.3625L29.247 59.5115Z" fill="#F8F9F9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6306 57.0236L29.247 59.5114L28.0091 68.3624L10.3927 65.8745L11.6306 57.0236Z" fill="#DDF3F3"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L35.1192 60.3408L33.8813 69.1917L22.137 67.5332L23.3749 58.6822Z" fill="#FFBE5D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.0091 68.3625L22.137 67.5332L23.3749 58.6822Z" fill="#FFA624"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.247 59.5115L35.1192 60.3408L34.7065 63.2911L28.8344 62.4618L29.247 59.5115Z" fill="#FFA624"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3749 58.6822L29.247 59.5115L28.8344 62.4618L22.9622 61.6326L23.3749 58.6822Z" fill="#EE9109"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8053 62.9241L22.5496 64.5827L22.137 67.533L10.3927 65.8745L10.8053 62.9241Z" fill="#C1E9E9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2939 66.2414L46.0382 67.9L45.6255 70.8503L33.8813 69.1917L34.2939 66.2414Z" fill="#DDF3F3"/>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

+15 -4
View File
@@ -30,12 +30,23 @@
cursor: default;
color: $gray-200;
opacity: 1;
box-shadow: none;
background-color: $gray-700;
background-color: transparent;
border: 2px solid transparent;
box-shadow:
0 1px 3px 0 rgba($black, 0.12),
0 1px 2px 0 rgba($black, 0.24);
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
padding: 4px 12px;
min-height: 32px;
max-height: 32px;
gap: 8px;
border-radius: 4px;
.svg {
color: $gray-300;
color: $gray-200;
}
}
@@ -123,6 +123,10 @@ h4 {
background-color: $purple-300 !important;
}
.bg-yellow-50 {
background-color: $yellow-50 !important;
}
.bg-white {
background-color: $white !important;
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#FFFFFF" fill-rule="nonzero">
<polygon points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 504 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -0,0 +1,29 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.1792 31.6843L46.8536 22.3769L23.918 28.6988L18.861 42.5218L44.341 58.5813L58.1792 31.6843Z" fill="#FF944C"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L46.1108 26.1328L36.2812 28.8422L46.6218 34.5148Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M30.2393 39.0304L26.4518 31.5515L36.2813 28.8422L30.2393 39.0304Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L36.2813 28.8422L30.2393 39.0304L46.6218 34.5148Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.1108 26.1328L46.6218 34.5148L53.8301 32.5279Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L26.4518 31.5516L30.2393 39.0304L23.0309 41.0173Z" fill="#FA8537"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M53.8301 32.5279L46.6218 34.5148L43.0424 53.79L53.8301 32.5279Z" fill="#FA8537"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M23.0309 41.0173L30.2393 39.0304L43.0425 53.79L23.0309 41.0173Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.6218 34.5148L30.2393 39.0304L43.0425 53.79L46.6218 34.5148Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M50.555 4.15937L47.026 0.420004L38.7773 1.59601L36.4144 6.17539L44.5675 12.8919L50.555 4.15937Z" fill="#FFBE5D"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L46.6034 1.6924L43.0682 2.1964L46.414 4.62854Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M40.5221 5.46854L39.5331 2.7004L43.0682 2.1964L40.5221 5.46854Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L43.0683 2.1964L40.5221 5.46855L46.414 4.62854Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25894L46.6034 1.6924L46.414 4.62854L49.0064 4.25894Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M37.9296 5.83815L39.5331 2.70041L40.5221 5.46855L37.9296 5.83815Z" fill="#FFA624"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M49.0064 4.25893L46.414 4.62853L44.3259 11.1688L49.0064 4.25893Z" fill="#FFA624"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M37.9297 5.83815L40.5221 5.46855L44.326 11.1688L37.9297 5.83815Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M46.414 4.62854L40.5221 5.46855L44.326 11.1688L46.414 4.62854Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.2986 16.7775L24.6513 8.36623L11.1016 3.94533L4.07056 9.19883L11.614 25.6769L27.2986 16.7775Z" fill="#FF6165"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L23.0573 10.0026L17.2502 8.10789L20.5864 14.3719Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M10.908 11.2141L11.4432 6.21322L17.2502 8.10789L10.908 11.2141Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L17.2502 8.10789L10.9081 11.2141L20.5864 14.3719Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L23.0573 10.0026L20.5864 14.3719L24.8449 15.7613Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M6.64955 9.82464L11.4432 6.21321L10.908 11.2141L6.64955 9.82464Z" fill="#F23035"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M24.8449 15.7613L20.5864 14.3719L12.5221 22.8464L24.8449 15.7613Z" fill="#F23035"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M6.64959 9.82464L10.9081 11.2141L12.5221 22.8463L6.64959 9.82464Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M20.5864 14.3719L10.9081 11.2141L12.5221 22.8463L20.5864 14.3719Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1,29 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.82083 31.6843L17.1464 22.3769L40.082 28.6988L45.139 42.5218L19.659 58.5813L5.82083 31.6843Z" fill="#24CC8F"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L17.8892 26.1328L27.7188 28.8422L17.3782 34.5148Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M33.7607 39.0304L37.5482 31.5515L27.7187 28.8422L33.7607 39.0304Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L27.7187 28.8422L33.7607 39.0304L17.3782 34.5148Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.8892 26.1328L17.3782 34.5148L10.1699 32.5279Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L37.5482 31.5516L33.7607 39.0304L40.9691 41.0173Z" fill="#1CA372"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M10.1699 32.5279L17.3782 34.5148L20.9576 53.79L10.1699 32.5279Z" fill="#1CA372"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M40.9691 41.0173L33.7607 39.0304L20.9575 53.79L40.9691 41.0173Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.3782 34.5148L33.7607 39.0304L20.9575 53.79L17.3782 34.5148Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.445 4.15937L16.974 0.420004L25.2227 1.59601L27.5856 6.17539L19.4325 12.8919L13.445 4.15937Z" fill="#925CF3"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L17.3966 1.6924L20.9318 2.1964L17.586 4.62854Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M23.4779 5.46854L24.4669 2.7004L20.9318 2.1964L23.4779 5.46854Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L20.9317 2.1964L23.4779 5.46855L17.586 4.62854Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25894L17.3966 1.6924L17.586 4.62854L14.9936 4.25894Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M26.0704 5.83815L24.4669 2.70041L23.4779 5.46855L26.0704 5.83815Z" fill="#4F2A93"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M14.9936 4.25893L17.586 4.62853L19.6741 11.1688L14.9936 4.25893Z" fill="#4F2A93"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M26.0703 5.83815L23.4779 5.46855L19.674 11.1688L26.0703 5.83815Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M17.586 4.62854L23.4779 5.46855L19.674 11.1688L17.586 4.62854Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7014 16.7775L39.3487 8.36623L52.8984 3.94533L59.9294 9.19883L52.386 25.6769L36.7014 16.7775Z" fill="#50B5E9"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L40.9427 10.0026L46.7498 8.10789L43.4136 14.3719Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M53.092 11.2141L52.5568 6.21322L46.7498 8.10789L53.092 11.2141Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L46.7498 8.10789L53.0919 11.2141L43.4136 14.3719Z" fill="white"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L40.9427 10.0026L43.4136 14.3719L39.1551 15.7613Z" fill="white"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L52.5568 6.21321L53.092 11.2141L57.3504 9.82464Z" fill="#46A7D9"/>
<path opacity="0.35" fill-rule="evenodd" clip-rule="evenodd" d="M39.1551 15.7613L43.4136 14.3719L51.4779 22.8464L39.1551 15.7613Z" fill="#46A7D9"/>
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M57.3504 9.82464L53.0919 11.2141L51.4779 22.8463L57.3504 9.82464Z" fill="white"/>
<path opacity="0.25" fill-rule="evenodd" clip-rule="evenodd" d="M43.4136 14.3719L53.0919 11.2141L51.4779 22.8463L43.4136 14.3719Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -117,7 +117,7 @@ export default {
closeWithAction () {
this.close();
setTimeout(() => {
this.$router.push({ name: 'achievements' });
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
}, 200);
},
},
@@ -81,7 +81,7 @@ export default {
watch: {
userIdentifier () {
this.isSearching = true;
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.$store.dispatch('admin:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.isSearching = false;
if (users.length === 1) {
this.loadUser(users[0]._id);
@@ -5,6 +5,12 @@
class="row"
>
<div class="form col-12">
<button
class="btn btn-danger mt-3 float-right"
@click="confirmDeleteHero"
>
Begin Member deletion
</button>
<basic-details
:user-id="hero._id"
:auth="hero.auth"
@@ -96,6 +102,53 @@
:reset-counter="resetCounter"
@clear-data="clearData"
/>
<b-modal
id="delete-member-modal"
title="Delete Member"
ok-title="Delete"
ok-variant="danger"
cancel-title="Cancel"
@ok="deleteHero"
>
<b-modal-body>
<p>
Are you sure you want to delete this member?
</p>
<p class="errorMessage">
Please note: This action cannot be undone!
</p>
<div class="ml-4">
<div class="form-check">
<input
id="deleteAccountCheck"
v-model="deleteHabiticaAccount"
class="form-check-input"
type="checkbox"
>
<label
class="form-check-label"
for="deleteAccountCheck"
>
Delete Habitica account
</label>
</div>
<div class="form-check">
<input
id="deleteAmplitudeCheck"
v-model="deleteAmplitudeData"
class="form-check-input"
type="checkbox"
>
<label
class="form-check-label"
for="deleteAmplitudeCheck"
>
Delete Amplitude data
</label>
</div>
</div>
</b-modal-body>
</b-modal>
</div>
</div>
</div>
@@ -184,6 +237,8 @@ export default {
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
deleteHabiticaAccount: true,
deleteAmplitudeData: true,
};
},
watch: {
@@ -249,6 +304,25 @@ export default {
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
confirmDeleteHero () {
if (this.hero._id === this.user._id) {
window.alert('You cannot delete your own account.');
return;
}
this.$root.$emit('bv::show::modal', 'delete-member-modal');
},
deleteHero () {
this.$store.dispatch('hall:deleteHero', {
uuid: this.hero._id,
deleteHabiticaAccount: this.deleteHabiticaAccount,
deleteAmplitudeData: this.deleteAmplitudeData,
}).then(() => {
this.$root.$emit('bv::hide::modal', 'delete-member-modal');
this.$router.push({ name: 'adminPanel' });
}).catch(err => {
window.alert(err);
});
},
hasUnsavedChanges (...comparisons) {
for (const index in comparisons) {
if (index && comparisons[index]) {
@@ -37,7 +37,11 @@
Party ID
</label>
<strong class="col-sm-9 col-form-label">
{{ groupPartyData._id }}
<router-link
:to="{'name': 'groupAdminGroup', 'params': {'groupId': groupPartyData._id}}"
>
{{ groupPartyData._id }}
</router-link>
</strong>
</div>
<div class="form-group row">
@@ -15,6 +15,25 @@
:class="{ 'open': expand }"
>
Subscription, Monthly Perks
<span
v-if="isSubscribed() && !isCancelled()"
class="text-success float-right ml-3"
>
Active
</span>
<span
v-else-if="isSubscribed() && isCancelled()"
class="text-success float-right ml-3"
>
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
</span>
<span
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
class="text-warning float-right ml-3"
>
Inactive
</span>
<b
v-if="hasUnsavedChanges && !expand"
class="text-warning float-right"
@@ -46,7 +65,7 @@
class="form-control"
type="text"
>
<option value="groupPlan">
<option value="Group Plan">
Group Plan
</option>
<option value="Stripe">
@@ -154,7 +173,11 @@
>
<div class="card-body">
<h6 class="card-title">
{{ group.name }}
<router-link
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
>
{{ group.name }}
</router-link>
<small class="float-right">{{ group._id }}</small>
</h6>
<p class="card-text">
@@ -245,8 +268,7 @@
</div>
</div>
<small
v-if="!hero.purchased.plan.dateTerminated
&& hero.purchased.plan.planId"
v-if="isSubscribed() && !isCancelled()"
class="text-success"
>
The subscription does not have a termination date and is active.
@@ -419,6 +441,79 @@
>
</div>
</div>
<div class="form-group row">
<h2>Payment Details</h2>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9 mb-3">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="getSubscriptionPaymentDetails"
>
Get Subscription Payment Details
</button>
</div>
</div>
<div
v-if="paymentDetails"
>
<div
v-for="(value, key) in paymentDetails"
:key="key"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
{{ getHumanReadablePaymentDetails(key).label }}:
<span
:id="`${key}_tooltip`"
v-b-tooltip.hover.right="getHumanReadablePaymentDetails(key).help"
class="info-icon"
>?</span>
</label>
<strong class="col-sm-9 col-form-label">
<span v-if="value === true">Yes</span>
<span v-else-if="value === false">No</span>
<span
v-else-if="value instanceof String && isDate(value)"
v-b-tooltip.hover="value"
>
{{ formatDate(value) }}
</span>
<span v-else-if="value === null">---</span>
<span v-else>{{ value }}</span>
</strong>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<a
v-if="hero.purchased.plan.paymentMethod === 'Google'"
class="btn btn-primary btn-sm"
target="_blank"
:href="playOrdersUrl"
>
Play Console
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
>
PayPal Dashboard
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
>
Stripe Dashboard
</a>
</div>
</div>
</div>
</div>
<div
v-if="expand"
@@ -474,17 +569,36 @@
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
.form-group {
margin-bottom: 0.4rem;
}
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
}
}
.info-icon {
font-size: 0.8rem;
color: $purple-400;
cursor: pointer;
margin-left: 0.2rem;
background-color: $gray-500;
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
}
.info-icon:hover {
background-color: $purple-400;
color: white;
}
}
</style>
<script>
@@ -495,6 +609,55 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner';
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
const humanReadablePaymentDetails = {
customerId: {
label: 'Customer ID',
help: 'The unique identifier for the customer in the payment system.',
},
purchaseDate: {
label: 'Purchase Date',
help: 'The date when the subscription was purchased or renewed.',
},
originalPurchaseDate: {
label: 'Original Purchase Date',
help: 'The date when the subscription was first purchased.',
},
productId: {
label: 'Product ID',
help: 'The identifier for the product associated with the subscription.',
},
transactionId: {
label: 'Transaction ID',
help: 'The unique identifier for the last transaction in the payment system.',
},
isCanceled: {
label: 'Is Canceled',
help: 'Indicates whether the subscription has been canceled by the user or the system.',
},
isExpired: {
label: 'Is Expired',
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
},
expirationDate: {
label: 'Termination Date',
help: 'The date when the subscription will expire or has expired.',
},
nextPaymentDate: {
label: 'Next Payment Date',
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
},
lastPaymentDate: {
label: 'Last Payment Date',
help: 'The date when the lastpayment was made for the subscription.',
},
failedPayments: {
label: 'Failed Payments',
help: 'Number of times the payment failed for this subscription.',
},
};
export default {
components: {
LoadingSpinner,
@@ -520,6 +683,7 @@ export default {
isConvertingToGroupPlan: false,
groupPlanID: '',
subscriptionBlocks,
paymentDetails: null,
};
},
computed: {
@@ -553,6 +717,9 @@ export default {
}
return terminationDate;
},
playOrdersUrl () {
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
},
},
methods: {
dateFormat (date) {
@@ -583,6 +750,20 @@ export default {
this.isConvertingToGroupPlan = true;
this.hero.purchased.plan.owner = '';
},
getSubscriptionPaymentDetails () {
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
.then(details => {
if (details) {
this.paymentDetails = details;
} else {
alert('No payment details found.');
}
})
.catch(error => {
console.error('Error fetching subscription payment details:', error);
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
});
},
saveClicked (e) {
e.preventDefault();
if (this.isConvertingToGroupPlan) {
@@ -601,6 +782,31 @@ export default {
this.$emit('changeUserIdentifier', id);
}
},
getHumanReadablePaymentDetails (key) {
return humanReadablePaymentDetails[key] || { label: key, help: '' };
},
isDate (date) {
return moment(date).isValid();
},
formatDate (date) {
return date ? moment(date).format('MM/DD/YYYY') : '---';
},
isSubscribed () {
console.log(this.hero.purchased.plan.customerId, this.hero.purchased.plan.dateTerminated);
return this.hero.purchased.plan
&& this.hero.purchased.plan.customerId
&& this.hero.purchased.plan.planId
&& this.hero.purchased.plan.paymentMethod
&& (
!this.hero.purchased.plan.dateTerminated
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
);
},
isCancelled () {
return this.hero.purchased.plan
&& this.hero.purchased.plan.dateTerminated
&& this.hero.purchased.plan.dateTerminated !== '';
},
},
};
</script>
@@ -226,7 +226,7 @@ export default {
}
},
async retrieveUserHistory () {
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
const history = await this.$store.dispatch('admin:getUserHistory', { userIdentifier: this.hero._id });
this.armoire = history.armoire;
this.questInviteResponses = history.questInviteResponses;
this.cron = history.cron;
@@ -8,6 +8,13 @@
>
{{ $t('adminPanel') }}
</router-link>
<router-link
v-if="hasPermission(user, 'groupSupport')"
class="nav-link"
:to="{name: 'groupAdmin'}"
>
{{ $t('groupAdmin') }}
</router-link>
<router-link
v-if="hasPermission(user, 'accessControl')"
class="nav-link"
@@ -0,0 +1,47 @@
<template>
<div class="form-group row">
<label class="col-sm-3 col-form-label"><slot name="label">{{ label }}</slot></label>
<div class="col-sm-9">
<slot>
<textarea
v-if="inputType === 'textarea'"
:value="value"
class="form-control"
:rows="rows"
@input="$emit('input', $event.target.value)"
></textarea>
<input
v-else
:value="value"
class="form-control"
:type="inputType"
@input="$emit('input', $event.target.value)"
>
</slot>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'value',
event: 'input',
},
props: {
label: {
type: String,
},
value: {
type: [String, Boolean],
},
inputType: {
type: String,
default: 'text',
},
rows: {
default: 3,
},
},
};
</script>
@@ -0,0 +1,45 @@
<template>
<form>
<form-row
v-model="group.name"
:label="$t('groupName')"
/>
<form-row
v-model="group.summary"
:label="$t('guildSummary')"
input-type="textarea"
/>
<form-row
v-model="group.description"
:label="$t('groupDescription')"
input-type="textarea"
rows="6"
/>
<form-row
v-model="group.bannedWordsAllowed"
:label="$t('bannedWordsAllowed')"
input-type="checkbox"
/>
<form-row
v-model="group.leaderOnly.challenges"
:label="$t('leaderOnlyChallenges')"
input-type="checkbox"
/>
</form>
</template>
<script>
import formRow from '@/components/admin/formRow.vue';
export default {
components: {
formRow,
},
props: {
group: {
type: Object,
required: true,
},
},
};
</script>
@@ -0,0 +1,69 @@
<template>
<div v-if="hasPermission(user, 'groupSupport')">
<h2>{{ group.name }}</h2>
<supportContainer
:title="$t('groupData')"
>
<groupData
:group="group"
/>
</supportContainer>
<supportContainer
:title="$t('groupPlanSubscription')"
/>
<supportContainer
v-if="group.type === 'party'"
:title="$t('questDetails')"
/>
<supportContainer
:title="$t('members')"
>
<members
:group="group"
/>
</supportContainer>
</div>
</template>
<script>
import { userStateMixin } from '../../../../mixins/userState';
import supportContainer from '../../supportContainer.vue';
import groupData from './groupData.vue';
import members from './members.vue';
export default {
components: {
supportContainer,
groupData,
members,
},
mixins: [userStateMixin],
data () {
return {
groupId: '',
group: {},
};
},
watch: {
groupId () {
this.loadGroup(this.groupId);
},
},
mounted () {
this.groupId = this.$route.params.groupId;
},
methods: {
clearData () {
this.group = {};
},
async loadGroup (groupId) {
this.$emit('changeGroupId', groupId);
this.group = await this.$store.dispatch('admin:getGroup', { groupId });
},
async updateGroup () {
await this.$store.dispatch('admin:updateGroup', { group: this.group });
this.$emit('groupSaved', this.group);
},
},
};
</script>
@@ -0,0 +1,29 @@
<template>
<form-row
:label="$t('groupLeader')"
>
<strong class="col-form-label">
<router-link
:to="{'name': 'adminPanelUser', 'params': {'userIdentifier': group.leader }}"
>
{{ group.leader }}
</router-link>
</strong>
</form-row>
</template>
<script>
import formRow from '@/components/admin/formRow.vue';
export default {
components: {
formRow,
},
props: {
group: {
type: Object,
required: true,
},
},
};
</script>
@@ -0,0 +1,93 @@
<template>
<div class="row standard-page col-12 d-flex justify-content-center">
<div class="group-admin-content">
<h1>{{ $t("groupAdmin") }}</h1>
<form
class="form-inline"
@submit.prevent="loadGroup(groupID)"
>
<div class="input-group col pl-0 pr-0">
<input
v-model="groupID"
class="form-control"
type="text"
placeholder="Group ID"
>
<div class="input-group-append">
<button
class="btn btn-primary"
type="button"
:disabled="!groupID"
@click="loadGroup(groupID)"
>
Load
</button>
</div>
</div>
</form>
<router-view
class="mt-3"
@changeGroupId="changeGroupId"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.uidField {
min-width: 45ch;
}
.input-group-append {
width:auto;
}
.group-admin-content {
flex: 0 0 800px;
max-width: 800px;
}
</style>
<script>
import VueRouter from 'vue-router';
import { mapState } from '@/libs/store';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
data () {
return {
groupID: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('groupAdmin'),
});
},
methods: {
changeGroupId (id) {
this.groupID = id;
},
async loadGroup (groupId) {
if (this.$router.currentRoute.name === 'groupAdminGroup') {
await this.$router.push({
name: 'groupAdmin',
});
}
await this.$router.push({
name: 'groupAdminGroup',
params: { groupId },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
this.$router.go();
}
});
},
},
};
</script>
@@ -0,0 +1,53 @@
<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
<slot name="title">
{{ title }}
</slot>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<slot></slot>
</div>
<div
v-if="expand && onSave"
class="card-footer"
>
<button
class="btn btn-primary mt-1"
@click="onSave"
>
{{ $t('save') }}
</button>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: false,
},
onSave: {
type: Function,
required: false,
},
},
data () {
return {
expand: false,
};
},
};
</script>
File diff suppressed because it is too large Load Diff
@@ -311,7 +311,7 @@
<input
id="passwordInput"
v-model="password"
class="form-control input-with-error"
class="form-control dark input-with-error"
type="password"
:placeholder="$t('password')"
:class="{'input-invalid': passwordInvalid, 'input-valid': passwordValid}"
@@ -323,7 +323,7 @@
{{ $t('minPasswordLength') }}
</div>
</div>
<div class="form-group">
<div class="form-group mb-4">
<label
v-once
for="confirmPasswordInput"
@@ -331,7 +331,7 @@
<input
id="confirmPasswordInput"
v-model="passwordConfirm"
class="form-control input-with-error"
class="form-control dark input-with-error"
type="password"
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
@@ -344,13 +344,14 @@
</div>
</div>
<div class="text-center">
<div
class="btn btn-info"
:enabled="!resetPasswordSetNewOneData.hasError"
<button
class="btn btn-info w-100"
:disabled="!password || !passwordConfirm
|| password !== passwordConfirm || resetPasswordSetNewOneData.hasError"
@click="resetPasswordSetNewOneLink()"
>
{{ $t('setNewPass') }}
</div>
</button>
</div>
</form>
</div>
@@ -672,7 +673,7 @@ export default {
this.login();
},
async forgotPasswordLink () {
forgotPasswordLink: debounce(async function forgotPassLink () {
if (!this.username) {
window.alert(this.$t('missingEmail')); // eslint-disable-line no-alert
return;
@@ -683,7 +684,7 @@ export default {
});
window.alert(this.$t('newPassSent')); // eslint-disable-line no-alert
},
}, 500),
async resetPasswordSetNewOneLink () {
if (!this.password) {
window.alert(this.$t('missingNewPassword')); // eslint-disable-line no-alert
@@ -43,9 +43,11 @@ export default {
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
const parseSettings = JSON.parse(AUTH_SETTINGS);
const userId = parseSettings ? parseSettings.auth.apiId : '';
const username = this.$store?.state?.user?.data?.auth?.local?.username || '';
return this.$t('accountSuspended', {
userId,
username,
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
});
},
@@ -7,7 +7,6 @@
@update-challenge="updateChallenge"
/>
<close-challenge-modal
:members="members"
:challenge-id="challenge._id"
:prize="challenge.prize"
:flag-count="challenge.flagCount"
@@ -72,32 +71,40 @@
</div>
<div class="col-12 col-md-6 text-right">
<div
class="box member-count"
class="box member-count p-2"
@click="showMemberModal()"
>
<div
class="svg-icon member-icon"
v-html="icons.memberIcon"
></div>
{{ challenge.memberCount }}
<div
v-once
class="details"
>
{{ $t('participantsTitle') }}
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon member-icon"
v-html="icons.memberIcon"
></div>
<span class="number">{{ challenge.memberCount }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('participantsTitle') }}
</div>
</div>
</div>
<div class="box">
<div
class="svg-icon gem-icon"
v-html="icons.gemIcon"
></div>
{{ challenge.prize || 0 }}
<div
v-once
class="details"
>
{{ $t('prize') }}
<div class="box prize-count p-2">
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon gem-icon"
v-html="icons.gemIcon"
></div>
<span class="number">{{ challenge.prize || 0 }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('prize') }}
</div>
</div>
</div>
</div>
@@ -304,7 +311,6 @@
.box {
display: inline-block;
padding: 1em;
border-radius: 2px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
@@ -314,22 +320,88 @@
text-align: center;
font-size: 20px;
vertical-align: bottom;
overflow: hidden;
position: relative;
&.member-count:hover {
cursor: pointer;
}
.box-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.icon-number-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.1em;
.number {
font-size: 20px;
font-weight: normal;
margin-left: 0.2em;
}
}
.svg-icon {
width: 30px;
display: inline-block;
margin-right: .2em;
vertical-align: bottom;
}
.details {
font-size: 12px;
margin-top: 0.4em;
color: $gray-200;
width: 100%;
padding: 0 4px;
line-height: 1.15;
word-break: break-word;
max-height: 2.3em;
overflow: visible;
}
&.member-count {
.icon-number-row {
.svg-icon {
width: 24px;
height: 24px;
}
.number {
font-size: 18px;
}
}
.details {
font-size: 11px;
line-height: 1.1;
max-height: 2.2em;
}
}
&.prize-count {
.icon-number-row {
.svg-icon {
width: 24px;
height: 24px;
}
.number {
font-size: 18px;
}
}
.details {
font-size: 11px;
line-height: 1.1;
max-height: 2.2em;
}
}
}
@@ -624,7 +696,6 @@ export default {
this.members = [];
},
closeChallenge () {
this.initialMembersLoad();
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
},
edit () {
@@ -4,6 +4,7 @@
id="close-challenge-modal"
:title="$t('endChallenge')"
size="md"
:hide-header="false"
>
<div
slot="modal-header"
@@ -15,6 +16,9 @@
>
{{ $t('endChallenge') }}
</h2>
<close-x
@close="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
/>
</div>
<div class="row text-center">
<span
@@ -28,28 +32,67 @@
class="col-12"
>
<div class="col-12">
<div class="support-habitica">
<!-- @TODO: Add challenge achievement badge here-->
<div class="badge-section">
<div
class="gems-left"
v-html="icons.gemsOrange"
></div>
<div
class="challenge-badge"
v-html="icons.endChallengeBadge"
></div>
<div
class="gems-right"
v-html="icons.gemsPurple"
></div>
</div>
</div>
<div class="col-12">
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
</div>
<div class="col-12">
<member-search-dropdown
:text="winnerText"
:members="members"
:challenge-id="challengeId"
@member-selected="selectMember"
/>
<div class="col-12 search-input-container">
<div class="search-input-wrapper">
<div
class="search-icon"
v-html="icons.search"
></div>
<input
v-model="searchTerm"
class="search-input"
type="text"
placeholder="@Username"
@input="searchMembers"
@focus="showResults = true"
@blur="handleBlur"
>
<div
v-if="showResults && filteredMembers.length > 0"
class="search-results"
>
<div
v-for="member in filteredMembers"
:key="member._id"
class="search-result-item"
@mousedown="selectMember(member)"
>
{{ getMemberDisplayName(member) }}
</div>
</div>
</div>
</div>
<div class="col-12">
<button
v-once
class="btn btn-primary"
class="btn award-winner-btn"
:class="{'has-winner': winner._id}"
:disabled="!winner._id"
@click="closeChallenge"
>
{{ $t('awardWinners') }}
<span>{{ $t('awardWinners') }}</span>
<div
class="gem-icon"
v-html="icons.gem"
></div>
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
</button>
</div>
</span>
@@ -60,14 +103,27 @@
</div>
</div>
<div class="col-12">
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
<strong
v-once
class="delete-challenge-text"
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
</div>
<div
v-once
class="col-12 refund-text"
>
{{ $t('deleteChallengeRefundDescription') }}
</div>
<div class="col-12">
<button
v-once
class="btn btn-danger"
class="btn btn-danger delete-challenge-btn"
@click="deleteChallenge()"
>
<div
class="svg-icon color delete-icon"
v-html="icons.deleteIcon"
></div>
{{ $t('deleteChallenge') }}
</button>
</div>
@@ -82,6 +138,7 @@
<style lang='scss'>
@import '@/assets/scss/colors.scss';
@import '@/assets/scss/button.scss';
#close-challenge-modal {
h2 {
@@ -94,26 +151,190 @@
.header-wrap {
width: 100%;
padding-top: 2em;
padding-top: 32px;
position: relative;
}
.support-habitica {
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
width: 325px;
height: 89px;
.modal-close {
position: absolute;
right: 16px;
top: 16px;
padding: 0;
margin: 0;
}
.search-input-container {
margin-top: 1em !important;
}
.search-input-wrapper {
position: relative;
width: 384px;
margin: 0 auto;
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-55%);
width: 16px;
height: 16px;
color: $gray-200;
pointer-events: none;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
height: 32px;
padding-left: 36px;
padding-right: 12px;
border: 1px solid $gray-400;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s ease, border-width 0.2s ease;
&:focus {
outline: none;
border: 2px solid $purple-400;
}
&::placeholder {
color: $gray-300;
}
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: $white;
border: 1px solid $gray-400;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.search-result-item {
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
&:hover {
background-color: $purple-600;
color: $purple-300;
}
}
}
}
.delete-challenge-text {
color: $maroon-50;
}
.refund-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 400;
color: $gray-50;
margin-top: 0.5em !important;
}
.delete-challenge-btn {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 24px;
display: inline-flex;
align-items: center;
gap: 8px;
.delete-icon {
width: 16px;
height: 16px;
display: inline-flex;
}
}
.award-winner-btn {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 4px 12px;
transition: all 0.2s ease;
&:not(:disabled) {
background-color: $white;
color: $gray-200;
border: 1px solid $gray-400;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
&.has-winner {
background-color: $purple-200;
color: $white;
border-color: $purple-200;
}
&:hover:not(.has-winner) {
background-color: $gray-700;
}
}
.gem-icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
color: $gems-color;
}
}
.badge-section {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin: -24px auto 0;
padding: 0.5rem 0;
.gems-left, .gems-right {
width: 64px;
height: 64px;
flex-shrink: 0;
}
.challenge-badge {
width: 48px;
height: 52px;
flex-shrink: 0;
}
}
.modal-footer, .modal-header {
border: none !important;
}
.modal-header {
padding: 0 !important;
}
.footer-wrap {
display: none;
}
.col-12 {
margin-top: 2em;
margin-top: 1.5em;
}
.col-12:first-child {
margin-top: 0;
}
.or {
@@ -123,21 +344,41 @@
margin-right: auto;
margin-left: auto;
font-weight: bold;
color: $gray-100;
}
}
</style>
<script>
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
import debounce from 'lodash/debounce';
import searchIcon from '@/assets/svg/for-css/search.svg?raw';
import deleteIcon from '@/assets/svg/delete.svg?raw';
import gemIcon from '@/assets/svg/gem.svg?raw';
import endChallengeBadge from '@/assets/svg/for-css/end_challenge_badge.svg?raw';
import gemsOrange from '@/assets/svg/for-css/orange100_red100_yellow100_gems.svg?raw';
import gemsPurple from '@/assets/svg/for-css/purple200_green10_blue100_gems.svg?raw';
import closeX from '@/components/ui/closeX';
export default {
components: {
memberSearchDropdown,
closeX,
},
props: ['challengeId', 'members', 'prize', 'flagCount'],
props: ['challengeId', 'prize', 'flagCount'],
data () {
return {
winner: {},
searchTerm: '',
showResults: false,
filteredMembers: [],
isSearching: false,
icons: Object.freeze({
search: searchIcon,
deleteIcon,
gem: gemIcon,
endChallengeBadge,
gemsOrange,
gemsPurple,
}),
};
},
computed: {
@@ -149,9 +390,58 @@ export default {
return this.flagCount > 0;
},
},
created () {
this.searchMembersDebounced = debounce(this.performSearch, 500);
},
methods: {
searchMembers () {
if (!this.searchTerm) {
this.filteredMembers = [];
this.isSearching = false;
return;
}
this.isSearching = true;
this.searchMembersDebounced();
},
async performSearch () {
if (!this.searchTerm) {
this.filteredMembers = [];
this.isSearching = false;
return;
}
const searchTerm = this.searchTerm.replace('@', '');
try {
const members = await this.$store.dispatch('members:getChallengeMembers', {
challengeId: this.challengeId,
searchTerm,
includeAllPublicFields: true,
});
this.filteredMembers = members.slice(0, 10);
} catch (err) {
this.filteredMembers = [];
} finally {
this.isSearching = false;
}
},
getMemberDisplayName (member) {
if (member.auth?.local?.username) {
return `@${member.auth.local.username}`;
}
return member.profile?.name || '';
},
selectMember (member) {
this.winner = member;
this.searchTerm = this.getMemberDisplayName(member);
this.showResults = false;
},
handleBlur () {
setTimeout(() => {
this.showResults = false;
}, 200);
},
async closeChallenge () {
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {
@@ -52,17 +52,21 @@
<div
v-if="!group.purchased.plan.dateTerminated
&& group.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary"
class="btn btn-primary mb-3"
@click="redirectToStripeEdit({groupId: group.id})"
>
{{ $t('subUpdateCard') }}
</div>
<div
v-if="!group.purchased.plan.dateTerminated"
class="btn btn-sm btn-danger"
@click="cancelSubscriptionConfirm({group: group})"
>
{{ $t('cancelGroupSub') }}
<div v-if="!group.purchased.plan.dateTerminated">
<div class="small gray-50 mb-3" v-once>
{{ $t('groupPlanBillingFYIShort') }}
</div>
<div
class="btn btn-sm btn-danger"
@click="cancelSubscriptionConfirm({group: group})"
>
{{ $t('cancelGroupSub') }}
</div>
</div>
</div>
</div>
@@ -82,9 +82,7 @@
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"
@@ -218,13 +218,19 @@
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
// somehow the browser felt like setting this 398px instead
// now its fixed to 400 :)
width: 400px;
max-width: 400px;
width: 100%;
margin-bottom: 1.5rem;
@media (max-width: 589px) {
max-width: 100%;
justify-content: center;
}
@media (max-width: 353px) {
gap: 0.25rem;
}
.quest-col {
::v-deep {
.item-wrapper {
@@ -251,6 +257,28 @@
::v-deep & {
.modal-dialog {
width: 448px !important;
max-width: calc(100vw - 20px);
margin: 0.5rem auto;
display: flex;
@media (max-width: 468px) {
width: 100% !important;
}
@media (max-width: 353px) {
width: 100% !important;
margin: 0.25rem auto;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
}
@@ -1,8 +1,8 @@
<template>
<div
class="banner d-flex align-items-center justify-content-between py-3 px-4"
id="privacy-banner"
v-if="!hidden"
id="privacy-banner"
class="banner d-flex align-items-center justify-content-between py-3 px-4"
>
<p
class="mr-3 mb-0"
@@ -1,37 +1,43 @@
<template>
<div
class="notification d-flex flex-column justify-content-center text-center"
class="notification d-flex justify-content-center align-items-center"
>
<strong
v-once
class="mx-auto mb-2"
<img
src="@/assets/images/gifts_start.svg"
class="gift-start"
alt=""
>
{{ $t('g1g1') }}
</strong>
<small
v-once
class="mx-4 mb-3"
>
{{ $t('g1g1Details') }}
</small>
<div
class="btn-secondary mx-auto d-flex"
@click="showSelectUser()"
>
<div
<div class="content-wrapper d-flex flex-column justify-content-center text-center">
<strong
v-once
class="m-auto"
class="mx-auto mb-2"
>
{{ $t('g1g1') }}
</strong>
<small
v-once
class="mx-4 mb-3"
>
{{ $t('g1g1Details') }}
</small>
<button
class="btn btn-secondary mx-auto"
@click="showSelectUser()"
>
{{ $t('sendGift') }}
</div>
</button>
</div>
<img
src="@/assets/images/gifts_start.svg"
class="gift-end"
alt=""
>
<div
class="notification-remove"
class="close-x"
@click.stop="remove()"
>
<div
v-once
class="svg-icon"
class="svg-icon svg-close"
v-html="icons.close"
></div>
</div>
@@ -41,51 +47,89 @@
<style lang='scss' scoped>
@import '@/assets/scss/colors.scss';
small, strong {
small {
color: $white;
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 14px;
line-height: 1.714;
letter-spacing: 0;
}
strong {
color: $white;
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 14px;
line-height: 1.714;
}
.notification {
background-image: url('@/assets/images/g1g1-notif.png');
background-image: url('@/assets/images/gifts_bg.svg');
background-size: cover;
background-position: center;
height: 10rem;
padding: 3rem;
padding: 0;
position: relative;
overflow: hidden;
white-space: normal;
cursor: pointer;
}
.notification-remove {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 24px;
top: 24px;
.svg-icon {
width: 10px;
height: 10px;
}
.content-wrapper {
flex: 1;
padding: 2rem;
z-index: 1;
}
.btn-secondary {
width: 5.75rem;
min-height: 1.5rem;
border-radius: 2px;
border-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
font-size: 12px;
font-weight: bold;
.gift-start {
height: 96px;
width: auto;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
z-index: 0;
}
.gift-end {
height: 96px;
width: auto;
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%) scaleX(-1);
z-index: 0;
}
.close-x {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
z-index: 2;
&:hover .svg-close {
opacity: 0.75;
}
.svg-close {
width: 18px;
height: 18px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
}
</style>
<script>
import closeIcon from '@/assets/svg/close-teal.svg?raw';
import { mapActions } from '@/libs/store';
import closeIcon from '@/assets/svg/close-white.svg?raw';
export default {
props: ['notification'],
props: ['notification', 'eventKey'],
data () {
return {
icons: Object.freeze({
@@ -94,11 +138,11 @@ export default {
};
},
methods: {
...mapActions({
readNotification: 'notifications:readNotification',
}),
remove () {
this.readNotification({ notificationId: this.notification.id });
if (this.eventKey) {
window.localStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
}
this.$emit('notification-removed');
},
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');
@@ -71,7 +71,7 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push({ name: 'achievements' });
this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`);
},
},
};
@@ -43,7 +43,7 @@ export default {
},
methods: {
action () {
this.$router.push({ name: 'stats' });
this.$router.push(`/profile/${this.$store.state.user.data._id}#stats`);
},
},
};
@@ -49,6 +49,12 @@
v-if="showOnboardingGuide"
:never-seen="hasSpecialBadge"
/>
<gift-one-get-one-notification
v-if="shouldShowG1g1"
:notification="g1g1Notification"
:event-key="g1g1EventKey"
@notification-removed="handleG1g1Removed"
/>
<component
:is="notification.type"
v-for="notification in notifications"
@@ -114,6 +120,7 @@
<script>
import * as quests from '@/../../common/script/content/quests';
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
import find from 'lodash/find';
import { mapState, mapActions } from '@/libs/store';
import notificationsIcon from '@/assets/svg/notifications.svg?raw';
import MenuDropdown from '../ui/customMenuDropdown';
@@ -151,6 +158,7 @@ export default {
CARD_RECEIVED,
CHALLENGE_INVITATION,
GIFT_ONE_GET_ONE,
GiftOneGetOneNotification: GIFT_ONE_GET_ONE,
GROUP_TASK_ASSIGNED,
GROUP_TASK_CLAIMED,
GROUP_TASK_NEEDS_WORK,
@@ -178,17 +186,14 @@ export default {
hasSpecialBadge: false,
quests,
openStatus: undefined,
g1g1Hidden: false,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
// NOTE: Those not listed here won't be shown in the notification panel!
handledNotifications: [
'NEW_STUFF',
'ITEM_RECEIVED',
'GIFT_ONE_GET_ONE',
'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION',
'PARTY_INVITATION',
@@ -207,7 +212,10 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
...mapState({
user: 'user.data',
currentEventList: 'worldState.data.currentEventList',
}),
notificationsOrder () {
// Returns a map of NOTIFICATION_TYPE -> POSITION
const orderMap = {};
@@ -286,9 +294,9 @@ export default {
return notifications;
},
// The total number of notification, shown inside the dropdown
notificationsCount () {
return this.notifications.length;
const g1g1Count = this.shouldShowG1g1 ? 1 : 0;
return this.notifications.length + g1g1Count;
},
hasUnseenNotifications () {
return this.notifications.some(notification => (notification.seen === false));
@@ -299,6 +307,30 @@ export default {
showOnboardingGuide () {
return !hasCompletedOnboarding(this.user);
},
currentG1g1Event () {
return find(this.currentEventList, event => event.promo === 'g1g1');
},
g1g1EventKey () {
if (!this.currentG1g1Event || !this.currentG1g1Event.start) return null;
const startDate = new Date(this.currentG1g1Event.start);
return `${startDate.getFullYear()}-${startDate.getMonth()}`;
},
shouldShowG1g1 () {
if (!this.currentG1g1Event) return false;
const eventKey = this.g1g1EventKey;
if (eventKey && window.localStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
return false;
}
return !this.g1g1Hidden;
},
g1g1Notification () {
return {
type: 'GIFT_ONE_GET_ONE',
id: `g1g1-event-${this.currentG1g1Event?.start || 'default'}`,
data: {},
seen: false,
};
},
},
mounted () {
const onboardingPanelState = getLocalSetting(CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE);
@@ -364,6 +396,9 @@ export default {
isActionable (notification) {
return this.actionableNotifications.indexOf(notification.type) !== -1;
},
handleG1g1Removed () {
this.g1g1Hidden = true;
},
},
};
@@ -176,7 +176,12 @@ export default {
}
},
showProfile (startingPage) {
this.$router.push({ name: startingPage });
const userId = this.$store.state.user.data._id;
let path = `/profile/${userId}`;
if (startingPage !== 'profile') {
path += `#${startingPage}`;
}
this.$router.push(path);
},
toLearnMore () {
this.$router.push({ name: 'subscription' });
@@ -454,17 +454,14 @@ export default {
},
isUserMentioned () {
const message = this.msg;
if (message.highlight) {
return true;
}
const { user } = this;
const displayName = user.profile.name;
const { username } = user.auth.local;
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
message.highlight = new RegExp(pattern, 'i').test(message.text);
if (!username) return false;
const usernamePattern = new RegExp(`@${escapeRegExp(username)}(?:\\b|(?=[^a-zA-Z0-9_]))`, 'i');
message.highlight = usernamePattern.test(message.text);
return message.highlight;
},
flagCountDescription () {
@@ -494,6 +491,9 @@ export default {
},
methods: {
mapProfileLinksToModal () {
if (!this.$refs?.markdownContainer) {
return;
}
const links = this.$refs.markdownContainer.getElementsByTagName('a');
for (let i = 0; i < links.length; i += 1) {
let link = links[i].pathname;
@@ -328,6 +328,8 @@ export default {
alreadyReadNotification,
nextCron: null,
handledNotifications,
isInitialLoadComplete: false,
pendingRebirthNotification: null,
};
},
computed: {
@@ -453,6 +455,18 @@ export default {
return this.runYesterDailies();
},
async showPendingRebirthModal () {
if (this.pendingRebirthNotification) {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
await axios.post('/api/v4/notifications/read', {
notificationIds: [this.pendingRebirthNotification.id],
});
this.pendingRebirthNotification = null;
}
},
showDeathModal () {
this.playSound('Death');
this.$root.$emit('bv::show::modal', 'death');
@@ -661,6 +675,18 @@ export default {
this.showLevelUpNotifications(this.user.stats.lvl);
}
this.handleUserNotifications(this.user.notifications);
this.isInitialLoadComplete = true;
const hasRebirthConfirmationFlag = localStorage.getItem('show-rebirth-confirmation') === 'true';
if (hasRebirthConfirmationFlag) {
localStorage.removeItem('show-rebirth-confirmation');
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
} else {
this.showPendingRebirthModal();
}
},
async handleUserNotifications (after) {
if (this.$store.state.isRunningYesterdailies) return;
@@ -700,8 +726,15 @@ export default {
this.$root.$emit('habitica:won-challenge', notification);
break;
case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
if (localStorage.getItem('show-rebirth-confirmation') === 'true') {
markAsRead = false;
} else if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
}
break;
case 'STREAK_ACHIEVEMENT':
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
@@ -197,9 +197,7 @@
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"
@@ -12,6 +12,12 @@
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
z-index: 9;
height: 3rem;
flex-wrap: wrap;
@media (max-width: 683px) {
height: auto;
min-height: 3rem;
}
}
.nav-link {
@@ -23,6 +29,19 @@
padding: 0.75rem;
color: $gray-50;
white-space: nowrap;
@media (max-width: 683px) {
padding: 0.5rem;
font-size: 13px;
flex: 1 1 auto;
min-width: fit-content;
}
@media (max-width: 576px) {
padding: 0.5rem 0.4rem;
font-size: 12px;
}
&.active {
color: $purple-300;
@@ -105,7 +105,7 @@ export default {
},
data () {
return {
privacyConsent: true,
privacyConsent: false,
};
},
methods: {
@@ -189,6 +189,7 @@
>
</p>
<div
v-if="paymentMethodLogo.icon"
class="svg svg-icon mb-4"
:class="paymentMethodLogo.class"
v-html="paymentMethodLogo.icon"
@@ -205,6 +206,13 @@
<div>{{ $t('subUpdateCard') }}</div>
</button>
</div>
<div
v-once
v-if="!hasGroupPlan"
class="small text-center mb-4"
>
{{ $t('subscriptionBillingFYIShort') }}
</div>
<div
v-if="purchasedPlanExtraMonthsDetails.months > 0"
class="extra-months green-10 py-2 px-3 mb-4"
@@ -407,6 +415,13 @@
<div class="purple-bar my-auto"></div>
</div>
<div class="d-flex flex-column align-items-center mt-3">
<div
v-once
v-if="!hasSubscription"
class="small gray-100 w-50 text-center mb-5"
>
{{ $t('subscriptionBillingFYI') }}
</div>
<div
v-once
class="svg-icon svg-gift-box mb-2"
@@ -631,7 +646,7 @@
background-color: $purple-400;
height: 1px;
width: 50%;
max-width: 432px;
max-width: 417px;
}
.purple-gradient {
@@ -654,6 +669,12 @@
margin-bottom: 16px;
}
.small {
font-size: 12px;
line-height: 1.67;
max-width: 874px;
}
.stats-card {
border-radius: 8px;
width: 192px;
@@ -12,14 +12,12 @@
class="staff col-6 p-0"
>
<div class="d-flex">
<router-link
<div
class="title"
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
>
{{ user.name }}
</router-link>
</div>
<div
v-if="user.type === 'Staff'"
class="svg-icon staff-icon ml-1"
v-html="icons.tierStaff"
></div>
@@ -269,12 +269,23 @@
.modal-dialog {
width: 448px;
max-width: calc(100vw - 20px);
box-sizing: border-box;
display: flex;
@media (max-width: 468px) {
width: 100%;
}
}
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.avatar {
@@ -346,7 +357,23 @@
.content {
text-align: center;
width: 448px;
width: 100%;
max-width: 448px;
margin: 0 auto;
@media (max-width: 468px) {
max-width: 100%;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.item-wrapper {
@@ -564,7 +591,7 @@
.limitedTime {
height: 32px;
width: 446px;
width: 100%;
font-size: 0.75rem;
margin: 24px 0 0 0;
background-color: $purple-300;
@@ -829,10 +856,17 @@ export default {
- ownedMounts
- ownedItems;
if (
petsRemaining < 0
&& !window.confirm(this.$t('purchasePetItemConfirm', { itemText: this.item.text })) // eslint-disable-line no-alert
) return;
if (petsRemaining < 0) {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t('purchasePetItemConfirm', { itemText: this.item.text }),
currency: this.item.currency,
cost: this.item.value * this.selectedAmountToBuy,
resolve,
});
});
if (!confirmed) return;
}
}
if (this.item.purchaseType === 'customization') {
@@ -844,15 +878,23 @@ export default {
this.purchased(this.item.text);
} else {
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
if (
shouldConfirmPurchase
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
) {
return;
if (shouldConfirmPurchase) {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.purchased(this.item.text);
if (this.item.key === 'rebirth_orb') {
localStorage.setItem('show-rebirth-confirmation', 'true');
}
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
if (this.item.key !== 'rebirth_orb') {
await this.purchased(this.item.text);
}
}
}
@@ -866,8 +908,8 @@ export default {
purchaseGems () {
this.$root.$emit('bv::show::modal', 'buy-gems');
},
togglePinned () {
this.isPinned = this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
async togglePinned () {
this.isPinned = await this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
if (!this.isPinned) {
this.text(this.$t('unpinnedItem', { item: this.item.text }));
@@ -76,7 +76,21 @@
:empty-item="false"
:show-popover="Boolean(ctx.item.text)"
@click="selectItem(ctx.item)"
/>
>
<template
slot="itemBadge"
slot-scope="slotProps"
>
<span
class="badge-top"
@click.prevent.stop="togglePinned(slotProps.item)"
>
<pin-badge
:pinned="slotProps.item.pinned"
/>
</span>
</template>
</shop-item>
</template>
</item-rows>
</div>
@@ -108,6 +122,16 @@
}
</style>
<style lang="scss">
.market .badge-pin:not(.pinned) {
display: none;
}
.market .item:hover .badge-pin {
display: block;
}
</style>
<script>
import find from 'lodash/find';
import shops from '@/../../common/script/libs/shops';
@@ -118,7 +142,9 @@ import Checkbox from '@/components/ui/checkbox';
import FilterGroup from '@/components/ui/filterGroup';
import FilterSidebar from '@/components/ui/filterSidebar';
import ItemRows from '@/components/ui/itemRows';
import PinBadge from '@/components/ui/pinBadge';
import ShopItem from '../shopItem';
import pinUtils from '@/mixins/pinUtils';
export default {
components: {
@@ -126,8 +152,10 @@ export default {
FilterGroup,
FilterSidebar,
ItemRows,
PinBadge,
ShopItem,
},
mixins: [pinUtils],
data () {
return {
searchText: null,
@@ -184,8 +212,12 @@ export default {
methods: {
customizationsItems (options = {}) {
const { category, searchBy } = options;
return category.items.filter(item => !searchBy
|| item.text.toLowerCase().includes(searchBy));
return category.items
.filter(item => !searchBy || item.text.toLowerCase().includes(searchBy))
.map(item => ({
...item,
pinned: this.isPinned(item),
}));
},
emptyClick (identifier, event) {
if (event.target.tagName !== 'A') return;
@@ -111,6 +111,22 @@
.modal-dialog {
width: 448px;
max-width: calc(100vw - 20px);
display: flex;
@media (max-width: 468px) {
width: 100%;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.modal-body {
@@ -0,0 +1,207 @@
<template>
<b-modal
id="purchase-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="purchase-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="currency-chip"
:class="currency"
>
<span
class="svg-icon icon-24"
v-html="icons[currency]"
></span>
<span class="cost-value">{{ cost }}</span>
</div>
<h2 class="modal-title">
{{ $t('confirmPurchase') }}
</h2>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="confirm()"
>
{{ $t('confirm') }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import svgGem from '@/assets/svg/gem.svg?raw';
import svgHourglass from '@/assets/svg/hourglass.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
currency: 'gems',
cost: 0,
resolveCallback: null,
icons: Object.freeze({
gems: svgGem,
hourglasses: svgHourglass,
}),
};
},
mounted () {
this.$root.$on('habitica:purchase-confirm', config => {
this.confirmationMessage = config.message;
this.currency = config.currency || 'gems';
this.cost = config.cost || 0;
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'purchase-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:purchase-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'purchase-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .purchase-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $purple-300;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.currency-chip {
display: flex;
align-items: center;
gap: 8px;
margin-top: 40px;
padding: 8px 20px;
border-radius: 20px;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
&.gems {
color: $gems-color;
background-color: rgba($green-10, 0.15);
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba($blue-10, 0.15);
}
.icon-24 {
width: 24px;
height: 24px;
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $purple-300;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
@@ -163,8 +163,38 @@
}
.modal-dialog {
margin-top: 8%;
width: 448px !important;
max-width: calc(100vw - 20px);
display: flex;
@media (max-width: 468px) {
width: 100% !important;
margin: 3rem auto 0.5rem;
}
@media (max-width: 353px) {
margin: 2.5rem auto 0.25rem;
}
}
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.content {
@@ -485,8 +515,12 @@ export default {
this.selectedAmountToBuy = 1;
this.$emit('change', $event);
},
buyItem () {
if (!this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)) {
async buyItem () {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
this.makeGenericPurchase(this.item, 'buyQuestModal', this.selectedAmountToBuy);
@@ -120,9 +120,9 @@
>
<ul>
<li>
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
- Web Developer
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'phillipthelen'}) }})
- Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
@@ -133,10 +133,6 @@
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
- Art, Community Management, Many Hats
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
- Designer
@@ -146,8 +142,12 @@
- Mobile Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
- Mobile Developer
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Kalista'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'fizself', realName: 'Hafiz'}) }}
- Developer
</li>
</ul>
<p v-html="$t('commGuidePara013')"></p>
@@ -156,7 +156,7 @@
<em>
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
Breadstrings, Megan, Blade, Daniel the Bard, deilann, shanaqui, Nakonana,
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
Dewines, Alys, Fox_town, MaybeSteveRogers, Cantras, and heyeilatan.
</em>
</p>
<h2 id="final">
@@ -45,6 +45,9 @@
<p class="gray-200">
{{ $t('billedMonthly') }}
</p>
<small class="gray-200">
{{ $t('groupPlanBillingFYI') }}
</small>
</div>
<div class="top-right"></div>
<div class="d-flex justify-content-between align-items-middle w-100 gap-72 mb-100">
@@ -114,6 +117,9 @@
<p class="gray-200">
{{ $t('billedMonthly') }}
</p>
<small class="gray-200">
{{ $t('groupPlanBillingFYI') }}
</small>
</div>
<div class="bot-right"></div>
</div>
@@ -174,6 +180,11 @@
line-height: 28px;
}
small {
font-size: 12px;
line-height: 1.67;
}
// Major layout elements
.bottom-banner {
@@ -11,12 +11,12 @@
<privacy-banner
class="privacy-banner"
/>
<div class="bg-purple-300 white">
<div class="bg-purple-300 white pt-5">
<div>
<div
id="intro-signup"
>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-center pb-5 mb-5">
<div class="w-33 mr-5 mt-5">
<img
src="@/assets/images/home/home-main@3x.png"
@@ -64,9 +64,11 @@
<li>sexual orientation; and</li>
<li>information collected from a known child.</li>
</ul>
<p><strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong></p>
<p>
<strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong>
</p>
<h3 id="section_1_1">
1.1 Information You Provide Directly
</h3>
@@ -617,7 +619,7 @@
7. General Audience Services
</h2>
<p>
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>, and we will delete that information from our databases.
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
</p>
<h2 id="section_8">
@@ -708,7 +710,7 @@
<p><strong><u>Nevada Residents</u></strong></p>
<p>
Nevada residents may opt out of the sale of certain covered information collected by operators of websites or online services. We currently do not sell covered information, as sale is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
Nevada residents may opt out of the sale of certain covered information collected by operators of websites or online services. We currently do not sell covered information, as sale is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>.
</p>
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
<p>
@@ -15,8 +15,8 @@
<router-view />
</div>
<div
id="bottom-background"
v-if="loginFlow"
id="bottom-background"
class="bg-purple-300"
>
<div class="seamless_mountains_demo_repeat"></div>
@@ -31,7 +31,10 @@
id="bottom-wrap"
class="purple-4"
>
<div id="bottom-background" v-if="!loginFlow">
<div
v-if="!loginFlow"
id="bottom-background"
>
<div class="seamless_mountains_demo_repeat"></div>
<div class="midground_foreground_extended2"></div>
</div>
@@ -104,9 +107,10 @@
footer, footer a {
background: transparent;
color: $purple-500;
&:hover {
color: $white;
}
}
footer a:hover {
color: $white;
}
h3 {
@@ -117,10 +121,6 @@
border-top-color: $purple-100;
}
.donate-text {
color: $purple-500;
}
.logo {
color: $purple-300;
}
@@ -129,42 +129,27 @@
color: $purple-500;
}
.social .d-flex:hover {
a {
color: $white;
}
svg {
fill: $white;
}
}
.social-circle {
background: $purple-50;
color: $purple-500;
.instagram svg {
svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.bluesky svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.facebook svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.tumblr svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
width: 24px;
height: 24px;
}
}
.btn-contribute {
background: $white;
box-shadow: none;
@@ -274,7 +259,8 @@ export default {
return 'purple-footer';
},
loginFlow () {
return ['login', 'register', 'username'].indexOf(this.$route.name) !== -1;
const loginRoutes = ['forgotPassword', 'login', 'register', 'resetPassword', 'username'];
return loginRoutes.indexOf(this.$route.name) !== -1;
},
showContentWrap () {
return this.$route.name !== 'news';
@@ -158,7 +158,7 @@
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
</p>
<p>
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href='mailto:admin@habitica.com'>ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
</p>
<p>
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
@@ -1,65 +1,45 @@
<template>
<b-modal
id="broken-task-modal"
title="Broken Challenge"
size="sm"
:hide-footer="true"
:hide-header="true"
modal-class="broken-task-confirm-modal"
centered
>
<div
v-if="brokenChallengeTask && brokenChallengeTask.challenge"
class="modal-body"
class="modal-content-wrapper"
>
<div
v-if="brokenChallengeTask.challenge.broken === 'TASK_DELETED'
|| brokenChallengeTask.challenge.broken === 'CHALLENGE_TASK_NOT_FOUND'"
>
<h2>{{ $t('brokenTask') }}</h2>
<div>
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.alertIcon"
></div>
<h2 class="modal-title">
{{ modalTitle }}
</h2>
<p class="modal-subtitle">
{{ modalSubtitle }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="unlink('keep')"
@click="keepAction()"
>
{{ $t('keepIt') }}
{{ keepButtonText }}
</button>
<button
class="btn btn-danger"
@click="removeTask(obj)"
@click="removeAction()"
>
{{ $t('removeIt') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_DELETED'">
<h2>{{ $t('brokenChallenge') }}</h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
{{ removeButtonText }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
class="btn-cancel"
@click="close()"
>
{{ $t('removeTasks') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_CLOSED'">
<h2 v-html="$t('challengeCompleted', {user: brokenChallengeTask.challenge.winner})"></h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
>
{{ $t('removeTasks') }}
{{ $t('cancel') }}
</button>
</div>
</div>
@@ -67,23 +47,175 @@
</b-modal>
</template>
<style scoped>
.modal-body {
padding-bottom: 2em;
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .broken-task-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
path {
fill: #DE3F3F;
}
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
<script>
import { mapActions } from '@/libs/store';
import notifications from '@/mixins/notifications';
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [notifications],
data () {
return {
brokenChallengeTask: {},
icons: Object.freeze({
alertIcon,
}),
};
},
computed: {
brokenType () {
return this.brokenChallengeTask.challenge?.broken;
},
isSingleTask () {
return this.brokenType === 'TASK_DELETED'
|| this.brokenType === 'CHALLENGE_TASK_NOT_FOUND';
},
brokenChallengeTaskCount () {
if (!this.brokenChallengeTask.challenge?.id) return 0;
const challengeId = this.brokenChallengeTask.challenge.id;
const tasksData = this.$store.state.tasks.data;
let count = 0;
['habits', 'dailys', 'todos', 'rewards'].forEach(type => {
if (tasksData[type]) {
count += tasksData[type].filter(
t => t.challenge && t.challenge.id === challengeId,
).length;
}
});
return count;
},
modalTitle () {
if (this.isSingleTask) {
return this.$t('brokenTask');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompleted');
}
return this.$t('brokenChallenge');
},
modalSubtitle () {
if (this.isSingleTask) {
return this.$t('brokenTaskDescription');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompletedDescription', { user: this.brokenChallengeTask.challenge?.winner });
}
return this.$t('brokenChallengeDescription');
},
keepButtonText () {
if (this.isSingleTask) {
return this.$t('keepIt');
}
return this.$t('keepTasks');
},
removeButtonText () {
if (this.isSingleTask) {
return this.$t('removeIt');
}
return this.$t('removeTasks');
},
},
mounted () {
this.$root.$on('handle-broken-task', task => {
this.brokenChallengeTask = { ...task };
@@ -99,8 +231,36 @@ export default {
unlinkOneTask: 'tasks:unlinkOneTask',
unlinkAllTasks: 'tasks:unlinkAllTasks',
}),
keepAction () {
if (this.isSingleTask) {
this.unlink('keep');
} else {
this.unlink('keep-all');
}
},
async removeAction () {
if (this.isSingleTask) {
await this.removeTask();
} else {
await this.unlink('remove-all');
}
},
async unlink (keepOption) {
if (keepOption.indexOf('-all') !== -1) {
if (keepOption === 'remove-all') {
const count = this.brokenChallengeTaskCount;
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
title: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
description: this.$t('brokenChallengeTaskCount', { count }),
message: this.$t('confirmDeleteTasks'),
buttonText: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
resolve,
});
});
if (!confirmed) return;
}
await this.unlinkAllTasks({
challengeId: this.brokenChallengeTask.challenge.id,
keep: keepOption,
@@ -122,8 +282,14 @@ export default {
});
this.close();
},
removeTask () {
if (!window.confirm('Are you sure you want to delete this task?')) return; // eslint-disable-line no-alert
async removeTask () {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDelete'),
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.brokenChallengeTask);
this.close();
},
@@ -87,7 +87,7 @@
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
scrollSensitivity="64"
scroll-sensitivity="64"
:delay-on-touch-only="true"
:delay="100"
@update="taskSorted"
@@ -0,0 +1,223 @@
<template>
<b-modal
id="delete-task-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="delete-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.alertIcon"
></div>
<h2 class="modal-title">
{{ displayTitle }}
</h2>
<p
v-if="description"
class="modal-description"
>
{{ description }}
</p>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-danger"
@click="confirm()"
>
{{ buttonText }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
taskType: '',
description: '',
customTitle: '',
customButtonText: '',
resolveCallback: null,
icons: Object.freeze({
alertIcon,
}),
};
},
computed: {
displayTitle () {
if (this.customTitle) return this.customTitle;
return this.$t('deleteType', { type: this.taskType });
},
buttonText () {
if (this.customButtonText) return this.customButtonText;
return this.displayTitle;
},
},
mounted () {
this.$root.$on('habitica:delete-task-confirm', config => {
this.confirmationMessage = config.message;
this.taskType = config.taskType || '';
this.description = config.description || '';
this.customTitle = config.title || '';
this.customButtonText = config.buttonText || '';
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'delete-task-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:delete-task-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'delete-task-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .delete-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
path {
fill: #DE3F3F;
}
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-description {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.modal-description + .modal-subtitle {
margin-top: 16px;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
+9 -2
View File
@@ -1177,9 +1177,16 @@ export default {
moveToBottom () {
this.$emit('moveTo', this.task, 'bottom');
},
destroy () {
async destroy () {
const type = this.$t(this.task.type);
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDeleteType', { type }),
taskType: type,
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
},
+160 -18
View File
@@ -35,7 +35,7 @@
</button>
<button
class="btn btn-secondary d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -150,25 +150,25 @@
<button
type="button"
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
d-flex flex-column justify-content-center align-items-center"
:class="!task.up ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired"
@click="toggleUpDirection()"
>
<div
class="habit-option-button no-transition
d-flex justify-content-center align-items-center mb-2"
d-flex justify-content-center align-items-center mb-2"
:class="task.up ? cssClass('bg') : ''"
>
<div
class="habit-option-icon svg-icon no-transition"
:class="task.up ? '' : 'disabled'"
:class="task.up ? '' : 'icon-disabled'"
v-html="icons.positive"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.up ? cssClass('icon') : 'disabled'"
:class="task.up ? cssClass('icon') : 'label-disabled'"
>
{{ $t('positive') }}
</div>
@@ -176,25 +176,25 @@
<button
type="button"
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
d-flex flex-column justify-content-center align-items-center"
:class="!task.down ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired"
@click="toggleDownDirection()"
>
<div
class="habit-option-button no-transition
d-flex justify-content-center align-items-center mb-2"
d-flex justify-content-center align-items-center mb-2"
:class="task.down ? cssClass('bg') : ''"
>
<div
class="habit-option-icon no-transition svg-icon negative mx-auto"
:class="task.down ? '' : 'disabled'"
:class="task.down ? '' : 'icon-disabled'"
v-html="icons.negative"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.down ? cssClass('icon') : 'disabled'"
:class="task.down ? cssClass('icon') : 'label-disabled'"
>
{{ $t('negative') }}
</div>
@@ -382,6 +382,45 @@
</div>
</div>
</div>
<div
v-if="showStatAssignment"
class="stat-assignment option mt-3"
>
<div class="form-group row">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedStat') }}</label>
<div class="col-12">
<div class="stat-dropdown-container">
<select-list
:items="statOptions"
:value="task.attribute"
key-prop="key"
active-key-prop="key"
@select="task.attribute = $event.key"
>
<template #item="{ item, button }">
<div class="stat-option-content">
<span
class="stat-option-title"
:class="item.key"
>
{{ $t(item.label) }}
</span>
<span
v-if="!button"
class="stat-option-description"
>
{{ $t(item.description) }}
</span>
</div>
</template>
</select-list>
</div>
</div>
</div>
</div>
<div
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
@@ -591,8 +630,8 @@
>
<button
class="btn btn-primary btn-footer
d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
d-flex align-items-center justify-content-center"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -881,12 +920,14 @@
}
}
.disabled {
.btn-disabled {
background-color: $white;
border: 2px solid transparent;
color: $gray-200;
line-height: 1.714;
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
cursor: not-allowed;
opacity: 0.6;
&:focus {
background-color: $white;
@@ -909,6 +950,87 @@
.streak-addon path {
fill: $gray-200;
}
.stat-dropdown-container {
.select-list {
.selectListItem {
margin-bottom: 0;
&:last-child {
margin-bottom: 0;
}
}
.selectListItem .dropdown-item {
padding: 8px 16px !important;
height: auto !important;
white-space: normal;
word-wrap: break-word;
&:hover,
&:focus {
background-color: rgba($purple-600, 0.25) !important;
}
}
.dropdown-toggle {
display: flex;
align-items: center;
.stat-option-content {
display: flex;
align-items: center;
.stat-option-title {
font-weight: normal;
color: $gray-50;
margin-bottom: 0;
}
}
}
}
.stat-option-content {
display: block;
width: 100%;
.stat-option-title {
display: block;
font-family: Roboto;
font-weight: 700;
font-size: 14px;
line-height: 1.71;
text-transform: capitalize;
margin-bottom: 4px;
&.str {
color: $maroon-100;
}
&.int {
color: $blue-50;
}
&.con {
color: $yellow-5;
}
&.per {
color: $purple-300;
}
}
.stat-option-description {
display: block;
font-family: Roboto;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
margin-bottom: 0;
}
}
}
</style>
<style lang="scss" scoped>
@@ -948,7 +1070,7 @@
height: 10px;
color: $white;
&.disabled {
&.icon-disabled {
color: $gray-200;
}
@@ -962,7 +1084,7 @@
font-weight: bold;
text-align: center;
&.disabled {
&.label-disabled {
color: $gray-100;
font-weight: normal;
}
@@ -1018,10 +1140,9 @@
border: 0;
}
.disabled .input-group-text {
.input-group-outer.disabled .input-group-text {
color: $gray-200;
}
</style>
<script>
@@ -1036,6 +1157,7 @@ import SelectMulti from './modal-controls/selectMulti';
import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty';
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import selectList from '@/components/ui/selectList';
import syncTask from '../../mixins/syncTask';
@@ -1059,6 +1181,7 @@ export default {
selectTranslatedArray,
toggleCheckbox,
lockableLabel,
selectList,
},
directives: {
markdown: markdownDirective,
@@ -1092,6 +1215,12 @@ export default {
con: 'constitution',
per: 'perception',
},
statOptions: [
{ key: 'str', label: 'strength', description: 'strTaskText' },
{ key: 'int', label: 'intelligence', description: 'intTaskText' },
{ key: 'con', label: 'constitution', description: 'conTaskText' },
{ key: 'per', label: 'perception', description: 'perTaskText' },
],
calendarHighlights: { dates: [new Date()] },
};
},
@@ -1185,6 +1314,12 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
showStatAssignment () {
return this.task.type !== 'reward'
&& !this.groupId
&& this.user.preferences.automaticAllocation === true
&& this.user.preferences.allocationMode === 'taskbased';
},
},
watch: {
task () {
@@ -1303,9 +1438,16 @@ export default {
}
this.$root.$emit('bv::hide::modal', 'task-modal');
},
destroy () {
async destroy () {
const type = this.$t(this.task.type);
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDeleteType', { type }),
taskType: type,
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
this.$root.$emit('bv::hide::modal', 'task-modal');
@@ -16,7 +16,7 @@
.badge-pin {
background-color: $white;
color: $gray-200;
color: $gray-100;
transition: none;
display: flex;
cursor: pointer;
@@ -32,8 +32,8 @@
}
.svg-icon {
width: 100%;
height: 100%;
width: 16px;
height: 16px;
}
}
@@ -12,7 +12,7 @@
<template #button-content>
<slot
name="item"
:item="selected || placeholder"
:item="selectedItem || placeholder"
:button="true"
>
<!-- Fallback content -->
@@ -134,6 +134,14 @@ export default {
}),
};
},
computed: {
selectedItem () {
if (this.activeKeyProp) {
return this.items.find(item => item[this.activeKeyProp] === this.selected);
}
return this.items.find(item => item === this.selected);
},
},
methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item.key || item.identifier;
@@ -25,8 +25,8 @@
type="checkbox"
:checked="isChecked"
:value="value"
@change="handleChange"
:disabled="disabled"
@change="handleChange"
>
<label
class="toggle-switch-label"
@@ -116,7 +116,7 @@
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $green-10;
background-color: $green-50;
}
.toggle-switch-inner:after {
@@ -1126,7 +1126,12 @@ export default {
this.loadUser();
this.oldTitle = this.$store.state.title;
this.handleExternalLinks();
this.selectPage(this.startingPage);
// Check if there's a hash in the URL to determine the starting page
let pageToSelect = this.startingPage;
if (window.location.hash && (window.location.hash === '#stats' || window.location.hash === '#achievements')) {
pageToSelect = window.location.hash.substring(1);
}
this.selectPage(pageToSelect);
this.$root.$on('habitica:report-profile-result', () => {
this.loadUser();
});
@@ -1211,10 +1216,15 @@ export default {
},
selectPage (page) {
this.selectedPage = page || 'profile';
window.history.replaceState(null, null, '');
const profileUserId = this.userId || this.userLoggedIn._id;
let newPath = `/profile/${profileUserId}`;
if (page !== 'profile') {
newPath += `#${page}`;
}
window.history.replaceState(null, null, newPath);
this.$store.dispatch('common:setTitle', {
section: this.$t('user'),
subSection: this.$t(this.startingPage),
subSection: this.$t(page),
});
},
getNextIncentive () {
File diff suppressed because it is too large Load Diff
+1 -11
View File
@@ -3,14 +3,10 @@ import isEqual from 'lodash/isEqual';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import amplitude from 'amplitude-js';
import { gtag, install } from 'ga-gtag';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true';
const GA_ID = import.meta.env.GA_ID;
const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production';
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
@@ -19,7 +15,7 @@ let analyticsReady = false;
function _getConsentedUser () {
const store = getStore();
const user = store.state.user.data;
if (!user?.preferences?.analyticsConsent || navigator.globalPrivacyControl) {
if (!user?.preferences?.analyticsConsent) {
return false;
}
return user;
@@ -69,10 +65,6 @@ function _gatherUserStats (properties) {
export function safeSetup (userId) {
if (analyticsLoading || analyticsReady) return;
analyticsLoading = true;
install(GA_ID, {
debug_mode: DEBUG_ENABLED || !IS_PRODUCTION,
user_id: userId,
});
amplitude.getInstance().init(AMPLITUDE_KEY, userId);
analyticsReady = true;
analyticsLoading = false;
@@ -90,7 +82,6 @@ export function track (properties, options = {}) {
// Track events on the server by default
if (trackOnClient === true) {
amplitude.getInstance().logEvent(properties.eventAction, properties);
gtag('event', properties.eventAction, properties);
} else {
const store = getStore();
store.dispatch('analytics:trackEvent', properties);
@@ -105,7 +96,6 @@ export function updateUser (properties = {}) {
// Use nextTick to avoid blocking the UI
Vue.nextTick(() => {
_gatherUserStats(properties);
gtag('set', 'user_properties', properties);
forEach(properties, (value, key) => {
const identify = new amplitude.Identify().set(key, value);
amplitude.getInstance().identify(identify);
+11 -2
View File
@@ -1,7 +1,16 @@
import habiticaMarkdown from 'habitica-markdown/withMentions';
import escapeRegExp from 'lodash/escapeRegExp';
export default function renderWithMentions (text, user) {
if (!text) return null;
const env = { userName: user.auth.local.username, displayName: user.profile.name };
return habiticaMarkdown.render(String(text), env);
const env = { userName: user.auth.local.username };
let html = habiticaMarkdown.render(String(text), env);
if (user.auth.local.username) {
const username = escapeRegExp(user.auth.local.username);
const regex = new RegExp(`(<span class="at-text">@)(${username})(</span>)`, 'gi');
html = html.replace(regex, (match, p1, p2, p3) => `${p1.replace('at-text', 'at-text at-highlight')}${p2}${p3}`);
}
return html;
}

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