Compare commits

..

53 Commits

Author SHA1 Message Date
Kalista Payne 6b56a4689e fix(lint): various corrections 2026-05-19 16:03:01 -05:00
Kalista Payne 966bfde06f WIP(links): replace hrefs in string tokens 2026-05-19 15:39:27 -05:00
Kalista Payne 9d23693a45 fix(links): add noopener noreferrer 2026-05-19 15:39:27 -05:00
Kalista Payne b57fb94579 Fiz/daily date discrepancy (#15656)
* Refactor(tasks): Centralize daily task start date normalization

* fix datepicker server day shifts by zeroing time

---------

Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-05-19 15:38:53 -05:00
Phillip Thelen 42805a2792 Improve googles ability to index the site (#15619)
* Add sitemap

* Add robots.txt

* create special entrypoint file for FAQ
2026-05-19 15:37:05 -05:00
Kalista Payne d7e7668255 5.48.0 2026-05-19 15:11:31 -05:00
Weblate a4fda59a69 Translated using Weblate (French)
Currently translated at 99.6% (3595 of 3607 strings)

Translated using Weblate (French)

Currently translated at 99.6% (3594 of 3607 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.0% (854 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (French)

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (French)

Currently translated at 99.6% (3593 of 3607 strings)

Translated using Weblate (French)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.4% (1892 of 3607 strings)

Translated using Weblate (Ukrainian)

Currently translated at 91.6% (798 of 871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 91.6% (798 of 871 strings)

Translated using Weblate (French)

Currently translated at 99.1% (944 of 952 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.4% (779 of 871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.4% (770 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.2% (769 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.3% (154 of 155 strings)

Translated using Weblate (French)

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.9% (757 of 871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 85.8% (748 of 871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (German)

Currently translated at 90.0% (18 of 20 strings)

Translated using Weblate (German)

Currently translated at 95.2% (278 of 292 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Bulgarian)

Currently translated at 9.0% (23 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Bulgarian)

Currently translated at 58.1% (160 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Spanish)

Currently translated at 99.3% (154 of 155 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (3597 of 3607 strings)

Translated using Weblate (Czech)

Currently translated at 86.1% (174 of 202 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Italian)

Currently translated at 85.8% (254 of 296 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (296 of 296 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.7% (738 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (944 of 952 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (952 of 952 strings)

Translated using Weblate (Dutch)

Currently translated at 79.2% (221 of 279 strings)

Translated using Weblate (German)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Dutch)

Currently translated at 80.0% (16 of 20 strings)

Translated using Weblate (German)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Dutch)

Currently translated at 98.4% (129 of 131 strings)

Translated using Weblate (Dutch)

Currently translated at 86.9% (254 of 292 strings)

Translated using Weblate (German)

Currently translated at 95.2% (278 of 292 strings)

Translated using Weblate (Dutch)

Currently translated at 86.8% (384 of 442 strings)

Translated using Weblate (Dutch)

Currently translated at 72.1% (2604 of 3607 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (German)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Dutch)

Currently translated at 79.5% (693 of 871 strings)

Translated using Weblate (German)

Currently translated at 97.8% (852 of 871 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (German)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (German)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.6% (3560 of 3607 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.4% (3552 of 3607 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3607 of 3607 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (3553 of 3607 strings)

Translated using Weblate (Polish)

Currently translated at 34.3% (87 of 253 strings)

Translated using Weblate (Polish)

Currently translated at 33.5% (85 of 253 strings)

Translated using Weblate (Polish)

Currently translated at 33.5% (85 of 253 strings)

Translated using Weblate (French)

Currently translated at 99.5% (3590 of 3607 strings)

Translated using Weblate (French)

Currently translated at 99.0% (3573 of 3607 strings)

Translated using Weblate (French)

Currently translated at 99.0% (3571 of 3607 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.8% (240 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 78.3% (2783 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Co-authored-by: ANGELO RICCIO <angeloriccio00.ar@gmail.com>
Co-authored-by: Amarin <amarin.nickel@posteo.de>
Co-authored-by: Antonio Lila Rusciano <antoniorusciano2005@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Dayra G.R <ale.ro.bless1501@gmail.com>
Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com>
Co-authored-by: Katarzyna Brudna <hisoillu6.06@gmail.com>
Co-authored-by: Lenka Pavlíčková <lenkapavlickova2@email.cz>
Co-authored-by: Magdalena Deshkova <magdalenadeshkova@gmail.com>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Maya B <maya.bl@icloud.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Riccardo DD <emvadraen@hotmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Stelio Passaris <habitica@stelio.net>
Co-authored-by: Sugo Gangotti <giacomo@ergonomia.it>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Val <3qes0hnzh@mozmail.com>
Co-authored-by: Vitaliy <italik.gr@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: kran fall <wisal50835@codoteam.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
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/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
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/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/it/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/nl/
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/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/it/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/es/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/it/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/it/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/de/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
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/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/es/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/it/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
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/Subscriber
Translation: Habitica/Tasks
2026-05-19 21:16:03 +02:00
Kalista Payne 3b9ffae625 fix(faq): pointer cursor 2026-05-12 16:48:49 -05:00
Kalista Payne dd413518f5 Fiz/summaries (#15653)
* dailies monthly weeks of month summary

update monthly day-of-week scheduling summary, add scheduling summary to task form, and add 5th week warning

* formatting

* scheduling warning and summary UI updates

* fix(svg): use extant icon

---------

Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-05-12 16:28:40 -05:00
Kalista Payne c581b88213 Summer 2026 Content (#15651)
* feat(content): June-August 2026

* fix(content): it's secretly a bright kite
2026-05-12 13:09:47 -05:00
Kalista Payne 0078d8f2b2 5.47.9 2026-05-12 11:38:53 -05:00
Weblate 54ea0aab18 Merge branch 'origin/develop' into Weblate. 2026-05-12 18:36:09 +02:00
Kalista Payne 24b2a5beb8 chore(git): update submodule 2026-05-12 11:34:50 -05:00
Kalista Payne fe9332dff4 fix(typo): missing word in Verdant Page item 2026-05-12 11:26:32 -05:00
Kalista Payne c6582e4c3c Squashed commit of the following:
commit 39e29846dd
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed May 6 22:45:25 2026 -0500

    fix(lint): no-undef

commit 75bbb5a88a
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed May 6 22:24:10 2026 -0500

    fix(paypal): cancel sub when payment skipped
2026-05-12 11:13:08 -05:00
Kalista Payne b4b7980eee Rate limit redux (#15650)
* Improve rate limit handling

* Improve rate limiter options

* Fix lint

* fixed url

---------

Co-authored-by: Phillip Thelen <phillip@thelen.space>
2026-05-12 11:11:00 -05:00
Weblate 6069fbd61f Translated using Weblate (French)
Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 74.3% (188 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Serbian)

Currently translated at 52.0% (143 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (94 of 94 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% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 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% (202 of 202 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% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Dutch)

Currently translated at 77.0% (215 of 279 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 99.2% (130 of 131 strings)

Translated using Weblate (Dutch)

Currently translated at 83.2% (243 of 292 strings)

Translated using Weblate (Dutch)

Currently translated at 86.8% (384 of 442 strings)

Translated using Weblate (Dutch)

Currently translated at 73.0% (2595 of 3551 strings)

Translated using Weblate (Dutch)

Currently translated at 98.9% (189 of 191 strings)

Translated using Weblate (Dutch)

Currently translated at 80.2% (699 of 871 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Serbian)

Currently translated at 32.3% (54 of 167 strings)

Translated using Weblate (Serbian)

Currently translated at 26.9% (45 of 167 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (German)

Currently translated at 99.6% (940 of 943 strings)

Translated using Weblate (German)

Currently translated at 99.6% (940 of 943 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (German)

Currently translated at 85.0% (17 of 20 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Serbian)

Currently translated at 26.9% (45 of 167 strings)

Translated using Weblate (Bulgarian)

Currently translated at 54.6% (515 of 943 strings)

Translated using Weblate (Bulgarian)

Currently translated at 53.7% (507 of 943 strings)

Translated using Weblate (Bulgarian)

Currently translated at 53.4% (504 of 943 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.0% (198 of 202 strings)

Translated using Weblate (Bulgarian)

Currently translated at 53.3% (503 of 943 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (244 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (244 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 91.3% (231 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 91.3% (231 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 86.1% (218 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 86.1% (218 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 85.7% (217 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 85.7% (217 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.9% (215 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.9% (215 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.5% (214 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.5% (214 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.1% (213 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 84.1% (213 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 83.7% (212 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 83.7% (212 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 83.3% (211 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 83.3% (211 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 82.6% (209 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 81.4% (206 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 81.4% (206 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 81.0% (205 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 81.0% (205 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 80.6% (204 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 80.6% (204 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 80.2% (203 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 80.2% (203 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 79.8% (202 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 79.8% (202 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 79.0% (200 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 79.0% (200 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (413 of 413 strings)

Co-authored-by: Antonio Lila Rusciano <antoniorusciano2005@gmail.com>
Co-authored-by: Carmen Ruiz Gomez <carmenruizgomez12@gmail.com>
Co-authored-by: Duggu Ghosh <duggu52d@gmail.com>
Co-authored-by: George <dyshlenko2@gmail.com>
Co-authored-by: Kris Fremen <me@krisfremen.com>
Co-authored-by: Lea Sophie Diekmann <dielea2012@gmail.com>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Oscar Trente <vincent@lamblot.net>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Stelio Passaris <habitica@stelio.net>
Co-authored-by: Tanishq Saini <tanishqsaini005@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: elsaaa <elsachaton@lavache.com>
Co-authored-by: kran fall <wisal50835@codoteam.com>
Co-authored-by: Павел <goncharovps@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
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/communityguidelines/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
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/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
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/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/en_GB/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-05-12 14:54:18 +02:00
Phillip Thelen 38a591bdd1 Trim group chat messages (#15646)
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2026-05-07 12:31:43 -05:00
Kalista Payne 2736d8acf3 5.47.8 2026-05-06 15:36:06 -05:00
Kalista Payne 8fe13dbb23 Revert "Improve rate limit handling (#15649)"
This reverts commit 1482f6c225.
2026-05-06 15:35:56 -05:00
Kalista Payne 4581bb9315 5.47.7 2026-05-06 14:57:31 -05:00
Weblate 2999212379 Merge branch 'origin/develop' into Weblate. 2026-05-06 21:55:54 +02:00
Weblate d2bd246e6e Translated using Weblate (Russian)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Russian)

Currently translated at 82.3% (2924 of 3551 strings)

Translated using Weblate (German)

Currently translated at 98.6% (3503 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 98.4% (188 of 191 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 82.2% (2919 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Russian)

Currently translated at 82.1% (2916 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Russian)

Currently translated at 75.4% (191 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Russian)

Currently translated at 98.0% (198 of 202 strings)

Translated using Weblate (Russian)

Currently translated at 75.0% (190 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Russian)

Currently translated at 75.0% (190 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 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% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 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% (202 of 202 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (German)

Currently translated at 98.6% (3502 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (15 of 15 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% (275 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 97.9% (3477 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 92.1% (3271 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 90.8% (3226 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 90.7% (3223 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 90.7% (3222 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 90.6% (3219 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 90.2% (3204 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.5% (3180 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.5% (3179 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.4% (3176 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.1% (3164 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.0% (3163 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.0% (3162 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 88.9% (3159 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 88.9% (3157 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 88.8% (3155 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 88.3% (3137 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 86.8% (3084 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 86.5% (3074 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 86.1% (3060 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 85.2% (3026 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 99.4% (866 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 99.3% (865 of 871 strings)

Translated using Weblate (Hebrew)

Currently translated at 92.4% (134 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 98.5% (858 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 95.5% (279 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 87.3% (255 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 83.9% (2982 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 83.8% (2978 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 83.8% (2978 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 83.8% (2978 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 95.7% (834 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Italian)

Currently translated at 81.3% (2889 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 95.7% (834 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 95.1% (829 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 94.7% (825 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 94.1% (820 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 93.3% (813 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 91.7% (799 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Italian)

Currently translated at 90.9% (792 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 81.1% (2883 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 89.6% (781 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 89.6% (781 of 871 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (278 of 279 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (3500 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Italian)

Currently translated at 86.3% (252 of 292 strings)

Translated using Weblate (Italian)

Currently translated at 81.1% (2883 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Italian)

Currently translated at 81.0% (2879 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 96.6% (427 of 442 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 75.0% (190 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 79.0% (200 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Italian)

Currently translated at 80.6% (2864 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 77.4% (196 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 89.2% (777 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Italian)

Currently translated at 80.6% (2864 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 60.8% (154 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 85.8% (748 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 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% (2 of 2 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Italian)

Currently translated at 87.4% (244 of 279 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 80.5% (2862 of 3551 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Italian)

Currently translated at 37.5% (95 of 253 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 96.3% (265 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 96.3% (265 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 96.3% (265 of 275 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Czech)

Currently translated at 92.1% (105 of 114 strings)

Translated using Weblate (Czech)

Currently translated at 91.2% (104 of 114 strings)

Translated using Weblate (Czech)

Currently translated at 85.6% (173 of 202 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.6% (3503 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 90.7% (3222 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 90.5% (3216 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 90.4% (3212 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 87.2% (3099 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 87.2% (3097 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 87.1% (3096 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 85.9% (3051 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (246 of 247 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (190 of 191 strings)

Translated using Weblate (Polish)

Currently translated at 70.0% (14 of 20 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (939 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 84.7% (3008 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (940 of 943 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (940 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 83.7% (2974 of 3551 strings)

Translated using Weblate (French)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Korean)

Currently translated at 49.0% (1740 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (3499 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.4% (3497 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 80.7% (2868 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 79.8% (2835 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 79.2% (2815 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 77.4% (2749 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (868 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 75.8% (2693 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (253 of 253 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 100.0% (871 of 871 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% (413 of 413 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (202 of 202 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% (275 of 275 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 74.9% (2662 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.0% (248 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Korean)

Currently translated at 41.8% (106 of 253 strings)

Translated using Weblate (Korean)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Korean)

Currently translated at 95.0% (19 of 20 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3475 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (246 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 73.8% (2621 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 93.5% (261 of 279 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.0% (248 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 73.8% (2621 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.0% (248 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 73.3% (2606 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.0% (248 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (202 of 202 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% (943 of 943 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 98.5% (3500 of 3551 strings)

Translated using Weblate (Ukrainian)

Currently translated at 66.0% (167 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 66.0% (167 of 253 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 55.0% (11 of 20 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 98.9% (90 of 91 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.0% (200 of 202 strings)

Translated using Weblate (Czech)

Currently translated at 84.5% (797 of 943 strings)

Translated using Weblate (Czech)

Currently translated at 59.2% (163 of 275 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3473 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 90.0% (18 of 20 strings)

Translated using Weblate (Japanese)

Currently translated at 98.4% (435 of 442 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.2% (284 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.9% (862 of 871 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 97.3% (111 of 114 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 70.0% (14 of 20 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.6% (151 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.0% (732 of 871 strings)

Translated using Weblate (Czech)

Currently translated at 84.0% (793 of 943 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.2% (1890 of 3551 strings)

Co-authored-by: Aleksandr <aichernyaev@yandex.ru>
Co-authored-by: Andrea Brunato <andrea.brunato@live.com>
Co-authored-by: Antonio Lila Rusciano <antoniorusciano2005@gmail.com>
Co-authored-by: Antonio Rusciano <antoniorusciano2005@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Daniel Costa Carvalho <danielcostacarvalho@gmail.com>
Co-authored-by: Dayra G.R <ale.ro.bless1501@gmail.com>
Co-authored-by: Deleted User <noreply+1630@weblate.org>
Co-authored-by: Igor <777igor93@gmail.com>
Co-authored-by: Illana Beatriz Rocha de Oliveira <dev.illanabeatriz@gmail.com>
Co-authored-by: Juhyung bang <juheng0912@gmail.com>
Co-authored-by: Karel <kcharlik@gmail.com>
Co-authored-by: Lenka Pavlíčková <lenkapavlickova2@email.cz>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Matej Boura <B.Matej@email.cz>
Co-authored-by: Omar Bertolla <scaram@icloud.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara Amit Cohen <saragirl93@gmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Stelio Passaris <habitica@stelio.net>
Co-authored-by: Sugo Gangotti <giacomo@ergonomia.it>
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: Wera <weramimi05@gmail.com>
Co-authored-by: Zhi Hao Li <zhihaoli000@gmail.com>
Co-authored-by: qazplm <513121975@qq.com>
Co-authored-by: Павел <goncharovps@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 김수빈 <kmsb0319@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/cs/
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/backgrounds/ko/
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/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/it/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/character/it/
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/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/death/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/death/it/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
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/fr/
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/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/it/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/it/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/it/
Translate-URL: https://translate.habitica.com/projects/habitica/noscript/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/it/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/it/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/it/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/it/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/it/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
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/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/he/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/it/
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/Limited
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
2026-05-06 21:55:43 +02:00
Kalista Payne 1482f6c225 Improve rate limit handling (#15649)
* Improve rate limit handling

* fix(lint): blank lines

---------

Co-authored-by: Phillip Thelen <phillip@thelen.space>
2026-05-06 14:47:33 -05:00
Fiz 1178da3a26 fix(quests): stuck "you were invited" banner after accept/reject (#15647)
wrap the conditional group update and the user save in a single transaction so a failure                                                                                                              between them can't leave members[uid] and RSVPNeeded out of sync
2026-05-05 11:09:30 -05:00
Kalista Payne 819ed2b355 Task dropdown and keyboard navigation fixes (#15648)
* fix(css): kebab z, focus highlights

* fix(nav): better tab behavior
2026-04-30 17:00:21 -05:00
Kalista Payne a92999fc11 5.47.6 2026-04-10 11:49:43 -05:00
Kalista Payne 3489b88752 fix(auth): downgrade helmet 2026-04-10 11:42:27 -05:00
Kalista Payne 94bda30385 5.47.5 2026-04-09 12:54:31 -05:00
Kalista Payne e8bbdc2cb8 fix(auth): disable broken CSP for now 2026-04-09 12:44:27 -05:00
Kalista Payne d465efaf96 fix(test): we have 1 item sets now 2026-04-08 15:35:47 -05:00
Kalista Payne 3a08de7ab3 fix(sorting): rerender task column for accuracy 2026-04-08 15:35:47 -05:00
Phillip Thelen e6ffd69148 feat(analytics): initial Habitica-owned solution 2026-04-08 15:35:47 -05:00
Kalista Payne 746fcfff49 Warning to avoid SPI (sensitive personal information) (#15638)
* feat(tasks): warn about adding SPI

* fix(links): unmangle, distinct jumps

* fix(spi): unlink

* fix(lint): punctuations
2026-04-08 15:35:47 -05:00
Kalista Payne 8aa343d390 5.47.4 2026-04-08 15:35:33 -05:00
Kalista Payne d80c43c82a test(ipn): log IPN data for troubleshooting 2026-04-08 15:35:03 -05:00
Kalista Payne 80e4b8617a fix(csp): habitica.com isn't *.habitica.com 2026-04-07 16:00:36 -05:00
Kalista Payne b3fac011a9 5.47.3 2026-04-07 15:04:01 -05:00
Weblate 8f4d871911 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (French)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.0% (854 of 871 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.8% (852 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3472 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (432 of 442 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3467 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (868 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (251 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (191 of 191 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 65.0% (13 of 20 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.2% (284 of 292 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 72.4% (2572 of 3551 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.4% (188 of 191 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 98.7% (860 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.5% (195 of 202 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (284 of 292 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (144 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (868 of 871 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.9% (187 of 189 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 95.9% (836 of 871 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (943 of 943 strings)

Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Jitske van Meerten <jitskevmeerten@hotmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
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/limited/uk/
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/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
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/Rebirth
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-04-07 16:51:25 +02:00
Hafiz 19373ce84d remove comment 2026-04-06 16:14:24 -05:00
Hafiz 55bfca20d9 comment 2026-04-06 16:04:43 -05:00
Kalista Payne 2ee2b05d1c Implement Content-Security-Policy (#15567)
* feat(security): implement CSP

* fix(csp): update helmet version to latest

* Squashed commit of the following:

commit cc6a35e61db07759c1f32716185543bc48bce760
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:27:50 2025 -0600

    fix(CSP): more Amazon domains

commit 985b86c29af866b2df942c21217d99390a2c6e92
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:18:08 2025 -0600

    fix(csp): more loggly allowance

commit 166bd315272f9c3a42652f3a026c27a88ed1a549
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:12:00 2025 -0600

    fix(csp): data, inline, some refactoring

commit 1a0a6c1806a53d43a7199bb2ef72cff610e908be
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:05:44 2025 -0600

    fix(CSP): override default script-src

commit 023d9886c835989da9c5901c168d66b572097dcf
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:56:24 2025 -0600

    fix(CSP): unsafe-eval in default-src

commit f51f0a0c93b60dfec7ce02be0ecd2587fc882fe0
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:52:14 2025 -0600

    fix(CSP): move trusted list to default-src

commit 83b2ba7688dea38abb651cf5c27482a7a3648374
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:38:05 2025 -0600

    fix(CSP): explicit habitica/aws in script-src

commit d5ca5172d5ad2fd8cec9402d7d2c9452c6ece7a1
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:31:38 2025 -0600

    fix(CSP): need escaped single quotes

commit c677a1ffeff5793b6da228924e68d2c2e47794b2
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:27:46 2025 -0600

    fix(CSP): unsafe-eval

commit 6ef35c3f7281c8426d9c333686be6bb65f00b3a8
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:15:07 2025 -0600

    fix(CSP): might need to skip entirely in dev but try no 'self'

commit 5759fb37d82fa61b474f01e9ce5e2dc461f6ceba
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 15:51:26 2025 -0600

    fix(csp): permit AWS in default-src

commit 9f238abf9373bc29715657b945b2247ef23c9224
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 5 17:22:25 2025 -0600

    fix(csp): update helmet version to latest

commit 9462e90f4f3058f4014137b3178b9751c5280e97
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Nov 25 09:27:05 2025 -0600

    feat(security): implement CSP

commit 72539f9ba3
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:53 2025 -0600

    5.42.2

commit dabd466719
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:48 2025 -0600

    Revert "Chat optimization (#15545)"

    This reverts commit 2917955ef0.

commit 8bf2304330
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:15:48 2025 -0600

    chore(event): G1G1 date tweaks

commit 6937dc4e4e
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 8 16:37:04 2025 -0600

    fix(subscription): couple more layout tweaks

* fix(csp): move unsafe-eval to default? ig?

* Revert "fix(csp): move unsafe-eval to default? ig?"

This reverts commit 90476cbf6c.

* fix(security): no unsafe! yay!

* fix(packages): remove webpack

* fix(lint): object destructuring

* fix(csp): remove Vue-Fragment

* wip(i18n): load Moment locale from cache

* fix(gulp): remove unneeded cache task

* fix(i18n): add Moment weekday abbrevs to translations

* fix(lint): destructuring
...why is this happening here and not develop lol

* fix(csp): add amplitude to whitelist

---------

Co-authored-by: Phillip Thelen <phillip@thelen.space>
2026-04-02 15:19:35 -05:00
Kalista Payne 3d3db1bdd9 Require email in social reg edge case (#15634)
* apply email passed via body if it is missing from apple profile

* Add Web UI to set email if apple does not provide one

* fix lint

* remove trailing space

* fix(register): show field if social auth without email

* fix(ux): add explanatory text

* fix(lint): max-len

* fix(data): remove unused field

* fix(auth): pass email around as necessary in Apple flow

* fix(auth): still wrong place argh

* Fix(auth): handle email in Apple registration flow

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-04-02 15:16:43 -05:00
Kalista Payne a5dff99fa1 5.47.2 2026-04-02 15:07:13 -05:00
Weblate 25435218e1 Translated using Weblate (Portuguese)
Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.8% (239 of 292 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.9% (2379 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 80.8% (236 of 292 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.3% (2357 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 82.0% (2912 of 3551 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (939 of 943 strings)

Translated using Weblate (Dutch)

Currently translated at 80.2% (699 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (413 of 413 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Korean)

Currently translated at 90.3% (103 of 114 strings)

Translated using Weblate (Korean)

Currently translated at 60.0% (12 of 20 strings)

Co-authored-by: Aleksandr <aichernyaev@yandex.ru>
Co-authored-by: Jildau Bras <jildaubras@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 윤태연 <bestpow123@naver.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
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/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
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/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Content
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-04-01 16:15:29 +02:00
Fiz 7746049bb4 fix(groups): expired guild plans not showing in upgrade modal (#15637)
use dateCreated instead of customerId to identify previously upgraded groups. Team cron clears customerId on expired plans as a query optimization, which makes them invisible to the upgrade flow. dateCreated is set on every group plan since the feature was introduced and is never cleared, so it reliably indicates a group was previously upgraded.
2026-03-31 12:17:42 -05:00
Kalista Payne 036bf43cf3 5.47.1 2026-03-27 14:55:06 -05:00
Weblate c4e0127a37 Merge branch 'origin/develop' into Weblate. 2026-03-27 20:51:22 +01:00
Kalista Payne 7ae059e243 fix(sprites): restore missing animations 2026-03-27 14:44:57 -05:00
Weblate 81b9a0b92d Translated using Weblate (Dutch)
Currently translated at 72.9% (2589 of 3551 strings)

Translated using Weblate (Czech)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Czech)

Currently translated at 99.4% (166 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.0% (18 of 20 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.4% (270 of 292 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (439 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (German)

Currently translated at 98.5% (3498 of 3551 strings)

Translated using Weblate (French)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Portuguese)

Currently translated at 54.4% (152 of 279 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2116 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 45.8% (116 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 45.8% (116 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 93.3% (14 of 15 strings)

Co-authored-by: Azuthrax <azuthrax@gmail.com>
Co-authored-by: Daniel Costa Carvalho <danielcostacarvalho@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Matej Boura <B.Matej@email.cz>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
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/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
Translation: Habitica/Achievements
Translation: Habitica/Challenge
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-03-27 19:07:01 +01:00
Kalista Payne a6e87452a6 fix(background): add price for mobile logic 2026-03-27 09:42:06 -05:00
Kalista Payne c40d384913 5.47.0 2026-03-24 12:58:53 -05:00
Weblate 65144aef28 Translated using Weblate (Spanish)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 90.7% (401 of 442 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (871 of 871 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 93.6% (816 of 871 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 78.3% (2782 of 3551 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 91.5% (185 of 202 strings)

Translated using Weblate (French)

Currently translated at 99.7% (869 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (943 of 943 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 75.8% (2693 of 3551 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (French)

Currently translated at 99.0% (863 of 871 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (French)

Currently translated at 98.0% (854 of 871 strings)

Translated using Weblate (French)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Japanese)

Currently translated at 99.3% (865 of 871 strings)

Translated using Weblate (Swedish)

Currently translated at 52.3% (146 of 279 strings)

Translated using Weblate (Swedish)

Currently translated at 80.0% (16 of 20 strings)

Translated using Weblate (Swedish)

Currently translated at 49.6% (1762 of 3551 strings)

Translated using Weblate (Swedish)

Currently translated at 81.6% (200 of 245 strings)

Translated using Weblate (Swedish)

Currently translated at 5.5% (14 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 70.7% (610 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 70.0% (14 of 20 strings)

Co-authored-by: Dayra G.R <ale.ro.bless1501@gmail.com>
Co-authored-by: Isabela de França <ifranceg@gmail.com>
Co-authored-by: Sam WIlson <sam.wils.2008@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es_419/
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/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/es/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sv/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2026-03-24 01:38:59 +01:00
434 changed files with 9614 additions and 8057 deletions
+1
View File
@@ -21,3 +21,4 @@ services:
timeout: 30s
start_period: 0s
retries: 30
+290 -547
View File
File diff suppressed because it is too large Load Diff
+4 -5
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.46.4",
"version": "5.48.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -19,7 +19,6 @@
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"bootstrap": "^4.6.2",
"bullmq": "^5.71.1",
"compression": "^1.8.1",
"cookie-session": "^2.1.1",
"coupon-code": "^0.4.5",
@@ -31,6 +30,7 @@
"eslint-plugin-mocha": "^5.0.0",
"express": "^4.21.1",
"express-basic-auth": "^1.2.1",
"express-sitemap-xml": "^3.1.0",
"express-validator": "^5.2.0",
"firebase-admin": "^12.1.1",
"glob": "^8.1.0",
@@ -44,7 +44,6 @@
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
"ioredis": "^5.10.1",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^2.1.5",
@@ -54,7 +53,7 @@
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"mongoose": "^8.23.0",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
@@ -68,6 +67,7 @@
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.2",
"redis": "^3.1.2",
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
@@ -77,7 +77,6 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
-560
View File
@@ -1,560 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import Amplitude from 'amplitude';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analyticsService', () => {
beforeEach(() => {
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
});
afterEach(() => {
sandbox.restore();
});
describe('#getServiceByEnvironment', () => {
it('returns mock methods when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
expect(analyticsService.getAnalyticsServiceByEnvironment())
.to.equal(analyticsService.mockAnalyticsService);
});
it('returns real methods when in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
.to.equal(analyticsService.track);
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
.to.equal(analyticsService.trackPurchase);
});
});
describe('#track', () => {
let eventType; let
data;
beforeEach(() => {
eventType = 'Cron';
data = {
category: 'behavior',
uuid: 'unique-user-id',
resting: true,
cronCount: 5,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about event', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5,
},
});
}));
it('sends english item name for gear if itemKey is provided', () => {
data.itemKey = 'headAccessory_special_foxEars';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Fox Ears',
},
});
});
});
it('sends english item name for egg if itemKey is provided', () => {
data.itemKey = 'Wolf';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Wolf Egg',
},
});
});
});
it('sends english item name for food if itemKey is provided', () => {
data.itemKey = 'Cake_Skeleton';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Bare Bones Cake',
},
});
});
});
it('sends english item name for hatching potion if itemKey is provided', () => {
data.itemKey = 'Golden';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Golden Hatching Potion',
},
});
});
});
it('sends english item name for quest if itemKey is provided', () => {
data.itemKey = 'atom1';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
},
});
});
});
it('sends english item name for purchased spell if itemKey is provided', () => {
data.itemKey = 'seafoam';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Seafoam',
},
});
});
});
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
balance: 12,
loginIncentives: 1,
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
balanceGemAmount: 48,
loginIncentives: 1,
},
});
});
});
});
});
describe('#trackPurchase', () => {
let data;
beforeEach(() => {
data = {
uuid: 'user-id',
sku: 'paypal-checkout',
paymentMethod: 'PayPal',
itemPurchased: 'Gems',
purchaseValue: 8,
purchaseType: 'checkout',
gift: false,
quantity: 1,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about purchase', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
gift: false,
itemPurchased: 'Gems',
paymentMethod: 'PayPal',
purchaseType: 'checkout',
quantity: 1,
sku: 'paypal-checkout',
},
});
}));
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
},
});
});
});
});
});
describe('mockAnalyticsService', () => {
it('has stubbed track method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
});
it('has stubbed trackPurchase method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
});
});
});
+116 -136
View File
@@ -13,7 +13,6 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common';
import * as analytics from '../../../../website/server/libs/analyticsService';
import { model as Group } from '../../../../website/server/models/group';
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
@@ -41,20 +40,17 @@ describe('cron', async () => {
},
},
});
sinon.spy(analytics, 'track');
});
afterEach(async () => {
if (clock !== null) clock.restore();
analytics.track.restore();
});
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
const timezoneUtcOffsetFromUserPrefs = -1;
await cron({
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
});
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
@@ -63,7 +59,7 @@ describe('cron', async () => {
it('resets user.items.lastDrop.count', async () => {
user.items.lastDrop.count = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.items.lastDrop.count).to.equal(0);
});
@@ -71,26 +67,11 @@ describe('cron', async () => {
it('increments user cron count', async () => {
const cronCountBefore = user.flags.cronCount;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
it('calls analytics', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', async () => {
beforeEach(async () => {
user.purchased.plan.customerId = 'subscribedId';
@@ -101,7 +82,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-12-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -112,7 +93,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-11-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -122,7 +103,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -131,7 +112,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -142,7 +123,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
@@ -150,7 +131,7 @@ describe('cron', async () => {
it('resets plan.dateUpdated on a new month', async () => {
const currentMonth = moment().startOf('month');
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
});
@@ -158,7 +139,7 @@ describe('cron', async () => {
it('increments plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
@@ -166,7 +147,7 @@ describe('cron', async () => {
it('increments plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(1);
});
@@ -175,7 +156,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(2);
});
@@ -184,7 +165,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(3);
});
@@ -196,7 +177,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
@@ -206,7 +187,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -214,7 +195,7 @@ describe('cron', async () => {
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.exist;
});
@@ -225,7 +206,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
@@ -264,7 +245,7 @@ describe('cron', async () => {
// Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness.
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -276,7 +257,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -311,7 +292,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -323,7 +304,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -358,7 +339,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user6, tasksByType, daysMissed, analytics,
user: user6, tasksByType, daysMissed,
});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -391,7 +372,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -403,7 +384,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(10);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -439,7 +420,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
@@ -452,7 +433,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
// subscription has been erased by now
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
@@ -471,7 +452,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -482,14 +463,14 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
it('does not reset plan.dateUpdated on a new month', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.dateUpdated).to.be.empty;
});
@@ -497,7 +478,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(0);
});
@@ -505,7 +486,7 @@ describe('cron', async () => {
it('does not increment plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(0);
});
@@ -513,7 +494,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
});
@@ -521,7 +502,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
@@ -530,7 +511,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -538,7 +519,7 @@ describe('cron', async () => {
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
});
@@ -564,7 +545,7 @@ describe('cron', async () => {
it('should make uncompleted todos redder', async () => {
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
});
@@ -573,7 +554,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.equal(valueBefore);
});
@@ -582,7 +563,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.todos).to.be.lengthOf(1);
@@ -608,7 +589,7 @@ describe('cron', async () => {
expect(user.tasksOrder.todos).to.be.lengthOf(3);
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
@@ -635,7 +616,7 @@ describe('cron', async () => {
const original = user.tasksOrder.todos; // Preserve the original order
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
let listsAreEqual = true;
@@ -675,7 +656,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
@@ -686,7 +667,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.exist;
});
@@ -696,14 +677,14 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
});
@@ -711,7 +692,7 @@ describe('cron', async () => {
it('should set tasks completed to false', async () => {
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -720,7 +701,7 @@ describe('cron', async () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -729,7 +710,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -739,7 +720,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -749,7 +730,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -759,7 +740,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
@@ -770,7 +751,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
});
@@ -784,7 +765,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -797,7 +778,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -808,7 +789,7 @@ describe('cron', async () => {
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
@@ -816,7 +797,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
@@ -829,7 +810,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
@@ -841,7 +822,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(0);
@@ -862,7 +843,7 @@ describe('cron', async () => {
tasksByType.dailys[1].frequency = 'daily';
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(48);
@@ -886,7 +867,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -897,7 +878,7 @@ describe('cron', async () => {
tasksByType.habits[0].up = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -909,7 +890,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.equal(1);
@@ -928,7 +909,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -941,7 +922,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -955,7 +936,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -964,7 +945,7 @@ describe('cron', async () => {
// should reset
daysMissed = 8;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -988,7 +969,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1002,7 +983,7 @@ describe('cron', async () => {
// should reset after user CDS
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1026,7 +1007,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1036,7 +1017,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1060,7 +1041,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1084,7 +1065,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1098,7 +1079,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1107,7 +1088,7 @@ describe('cron', async () => {
// should reset
daysMissed = 32;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1132,7 +1113,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1156,7 +1137,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1166,7 +1147,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1199,7 +1180,7 @@ describe('cron', async () => {
user.stats.lvl = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.exp).to.have.lengthOf(1);
@@ -1212,7 +1193,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(1);
@@ -1224,7 +1205,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(0);
@@ -1238,7 +1219,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1256,7 +1237,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1280,7 +1261,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1307,7 +1288,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1333,7 +1314,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1360,7 +1341,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1381,7 +1362,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1401,7 +1382,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1420,7 +1401,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.be.greaterThan(mpBefore);
@@ -1436,7 +1417,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(mpBefore);
@@ -1449,7 +1430,7 @@ describe('cron', async () => {
user.stats.mp = 120;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
@@ -1482,7 +1463,7 @@ describe('cron', async () => {
it('resets user progress', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).to.equal(0);
@@ -1491,7 +1472,7 @@ describe('cron', async () => {
it('applies the user progress', async () => {
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
});
@@ -1529,19 +1510,19 @@ describe('cron', async () => {
describe('login incentives', async () => {
it('increments incentive counter each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
user.lastCron = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
});
it('pushes a notification of the day\'s incentive each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.notifications.length).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
@@ -1549,13 +1530,13 @@ describe('cron', async () => {
it('replaces previous notifications', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
@@ -1566,7 +1547,7 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
daysMissed = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
@@ -1574,14 +1555,14 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
it('awards user bard robes if login incentive is 1', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
@@ -1591,7 +1572,7 @@ describe('cron', async () => {
it('awards user incentive backgrounds if login incentive is 2', async () => {
user.loginIncentives = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
expect(user.purchased.background.blue).to.eql(true);
@@ -1605,7 +1586,7 @@ describe('cron', async () => {
it('awards user Bard Hat if login incentive is 3', async () => {
user.loginIncentives = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(3);
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
@@ -1615,7 +1596,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
user.loginIncentives = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(4);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1625,7 +1606,7 @@ describe('cron', async () => {
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
user.loginIncentives = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(5);
@@ -1639,7 +1620,7 @@ describe('cron', async () => {
it('awards user moon quest if login incentive is 7', async () => {
user.loginIncentives = 6;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(7);
expect(user.items.quests.moon1).to.eql(1);
@@ -1649,7 +1630,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
user.loginIncentives = 9;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(10);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1659,7 +1640,7 @@ describe('cron', async () => {
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
user.loginIncentives = 13;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(14);
@@ -1673,7 +1654,7 @@ describe('cron', async () => {
it('awards user a bard instrument if login incentive is 18', async () => {
user.loginIncentives = 17;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(18);
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
@@ -1683,7 +1664,7 @@ describe('cron', async () => {
it('awards user second moon quest if login incentive is 22', async () => {
user.loginIncentives = 21;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(22);
expect(user.items.quests.moon2).to.eql(1);
@@ -1693,7 +1674,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
user.loginIncentives = 25;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(26);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1703,7 +1684,7 @@ describe('cron', async () => {
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
user.loginIncentives = 29;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(30);
@@ -1718,7 +1699,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
user.loginIncentives = 34;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(35);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1728,7 +1709,7 @@ describe('cron', async () => {
it('awards user the third moon quest if login incentive is 40', async () => {
user.loginIncentives = 39;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(40);
expect(user.items.quests.moon3).to.eql(1);
@@ -1738,7 +1719,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
user.loginIncentives = 44;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(45);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1748,7 +1729,7 @@ describe('cron', async () => {
it('awards user a saddle if login incentive is 50', async () => {
user.loginIncentives = 49;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(50);
expect(user.items.food.Saddle).to.eql(1);
@@ -1766,7 +1747,6 @@ describe('cron wrapper', () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analytics;
});
afterEach(() => {
+25 -22
View File
@@ -1,9 +1,9 @@
/* eslint-disable global-require */
import got from 'got';
import nconf from 'nconf';
import requireAgain from 'require-again';
import { TAVERN_ID } from '../../../../website/server/models/group';
import { defer } from '../../../helpers/api-unit.helper';
import worker from '../../../../website/server/libs/worker';
function getUser () {
return {
@@ -127,7 +127,7 @@ describe('emails', () => {
let sendTxn = null;
beforeEach(() => {
sandbox.stub(worker, 'sendJob').returns(defer().promise);
sandbox.stub(got, 'post').returns(defer().promise);
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('IS_PROD').returns(true);
@@ -149,12 +149,13 @@ describe('emails', () => {
};
sendTxn(mailingInfo, emailType);
expect(worker.sendJob).to.be.called;
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
identifier: emailType,
data: {
emailType: sinon.match.same(emailType),
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
},
},
}));
});
@@ -167,7 +168,7 @@ describe('emails', () => {
};
sendTxn(mailingInfo, emailType);
expect(worker.sendJob).not.to.be.called;
expect(got.post).not.to.be.called;
});
it('throws error when mail target is only a string', async () => {
@@ -232,12 +233,13 @@ describe('emails', () => {
const mailingInfo = getUser();
sendTxn(mailingInfo, emailType);
expect(worker.sendJob).to.be.called;
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
identifier: emailType,
data: {
emailType: sinon.match.same(emailType),
to: sinon.match(val => val[0]._id === mailingInfo._id),
expect(got.post).to.be.called;
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
to: sinon.match(val => val[0]._id === mailingInfo._id),
},
},
}));
});
@@ -251,14 +253,15 @@ describe('emails', () => {
const variables = [];
sendTxn(mailingInfo, emailType, variables);
expect(worker.sendJob).to.be.called;
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
identifier: emailType,
data: {
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
&& value[0].vars[0].name === 'RECIPIENT_NAME'
expect(got.post).to.be.called;
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'),
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
&& value[0].vars[0].name === 'RECIPIENT_NAME'
&& value[0].vars[1].name === 'RECIPIENT_UNSUB_URL', 'matches personal variables'),
},
},
}));
});
+100
View File
@@ -0,0 +1,100 @@
import nconf from 'nconf';
import requireAgain from 'require-again';
import { model as User } from '../../../../website/server/models/user';
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
describe('localAnalytics', () => {
let user;
let localAnalytics;
before(() => {
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
});
beforeEach(async () => {
user = new User({
auth: {
local: {
username: 'username',
email: 'email@example.com',
},
},
registeredThrough: 'habitica-web',
});
});
describe('trackRegistrationEvent', () => {
afterEach(async () => {
await RegistrationEventModel.deleteMany({});
});
it('creates a registration event when a user registers', async () => {
user._id = '00000000-0000-0000-0000-000000000001';
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(1);
expect(registrationEvents[0]).to.have.property('userId', user._id);
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
});
it('saves the correct data to the database', async () => {
user._id = '00000000-0000-0000-0000-000000000002';
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
});
describe('trackSubscriptionEvent', () => {
afterEach(async () => {
await SubscriptionEventModel.deleteMany({});
});
it('creates a subscription event when a user subscribes', async () => {
user._id = '00000000-0000-0000-0000-000000000003';
await localAnalytics.trackSubscriptionEvent({
eventType: 'subscribed',
user,
paymentMethod: 'stripe',
customerId: 'cus_123',
planId: 'plan_123',
});
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
expect(subscriptionEvents).to.have.lengthOf(1);
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
});
it('creates a subscription event with cancellation reason when a user cancels', async () => {
user._id = '00000000-0000-0000-0000-000000000004';
await localAnalytics.trackSubscriptionEvent({
eventType: 'cancelled',
user,
paymentMethod: 'stripe',
customerId: 'cus_456',
planId: 'plan_456',
cancellationReason: 'No longer needed',
});
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
});
});
});
+64 -47
View File
@@ -3,7 +3,6 @@ import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import common from '../../../../../website/common';
import api from '../../../../../website/server/libs/payments/payments';
import * as analytics from '../../../../../website/server/libs/analyticsService';
import * as notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-integration/v3';
@@ -13,6 +12,7 @@ import {
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
describe('payments/index', () => {
let user;
@@ -36,8 +36,6 @@ describe('payments/index', () => {
sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
sandbox.stub(analytics.mockAnalyticsService, 'track');
sandbox.stub(notifications, 'sendNotification');
data = {
@@ -97,6 +95,16 @@ describe('payments/index', () => {
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
expect(subscriptionEvent).to.have.property('userId', recipient._id);
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan;
@@ -298,28 +306,6 @@ describe('payments/index', () => {
expect(notifications.sendNotification).to.be.calledOnce;
});
it('tracks subscription purchase as gift', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: true,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('No Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([]);
@@ -455,6 +441,16 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('ipAddress');
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
@@ -543,29 +539,24 @@ describe('payments/index', () => {
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
});
it('tracks subscription purchase', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: false,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('Upgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
@@ -608,6 +599,23 @@ describe('payments/index', () => {
});
context('Downgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
@@ -1136,6 +1144,15 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('tracks subscription events', async () => {
await api.cancelSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('userId', user._id);
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
@@ -1,50 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analytics middleware', () => {
let res; let req; let
next;
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches analytics object to res', () => {
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics).to.exist;
});
it('attaches stubbed methods for non-prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
});
it('attaches real methods for prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
});
});
+24 -12
View File
@@ -32,7 +32,8 @@ describe('rateLimiter middleware', () => {
it('is disabled when the env var is not defined', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
@@ -43,7 +44,8 @@ describe('rateLimiter middleware', () => {
it('is disabled when the env var is an not "true"', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
@@ -55,7 +57,8 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
@@ -77,7 +80,8 @@ describe('rateLimiter middleware', () => {
sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.')));
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce;
@@ -92,7 +96,8 @@ describe('rateLimiter middleware', () => {
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = 'abc';
await attachRateLimiter(req, res, next);
@@ -107,7 +112,8 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = 'das';
await attachRateLimiter(req, res, next);
@@ -124,7 +130,8 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next);
@@ -140,7 +147,8 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = '';
await attachRateLimiter(req, res, next);
@@ -156,7 +164,8 @@ describe('rateLimiter middleware', () => {
it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
// call for 31 times
for (let i = 0; i < 31; i += 1) {
@@ -180,7 +189,8 @@ describe('rateLimiter middleware', () => {
it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.ip = 1;
await attachRateLimiter(req, res, next);
@@ -210,7 +220,8 @@ describe('rateLimiter middleware', () => {
it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.path = '/api/v4/user/auth/local/register';
req.ip = 1;
@@ -241,7 +252,8 @@ describe('rateLimiter middleware', () => {
it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.ip = 1;
await attachRateLimiter(req, res, next);
+54
View File
@@ -6,6 +6,8 @@ import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
MAX_CHAT_COUNT,
MAX_SUBBED_GROUP_CHAT_COUNT,
INVITES_LIMIT,
model as Group,
} from '../../../../website/server/models/group';
@@ -18,6 +20,7 @@ import {
import * as email from '../../../../website/server/libs/email';
import { TAVERN_ID } from '../../../../website/common/script/constants';
import shared from '../../../../website/common';
import { chatModel as Chat } from '../../../../website/server/models/message';
describe('Group Model', () => {
let party; let questLeader; let participatingMember;
@@ -1356,6 +1359,29 @@ describe('Group Model', () => {
});
});
describe('#getEffectiveChatLimit', () => {
it('returns the correct chat limit', () => {
const group = new Group();
expect(group.getEffectiveChatLimit()).to.eql(MAX_CHAT_COUNT);
});
it('returns the passed limit if it is lower than the max', () => {
const group = new Group();
expect(group.getEffectiveChatLimit(10)).to.eql(10);
});
it('returns the max if the passed limit is higher', () => {
const group = new Group();
expect(group.getEffectiveChatLimit(MAX_CHAT_COUNT + 10)).to.eql(MAX_CHAT_COUNT);
});
it('returns the max for group plans', () => {
const group = new Group();
group.purchased.plan.customerId = '110002222333';
expect(group.getEffectiveChatLimit()).to.eql(MAX_SUBBED_GROUP_CHAT_COUNT);
});
});
describe('#sendChat', () => {
beforeEach(() => {
sandbox.spy(User, 'updateOne');
@@ -1462,6 +1488,34 @@ describe('Group Model', () => {
});
});
describe('#trimChat', () => {
it('Only checks last message when not enough messages to trim', async () => {
sandbox.spy(Chat, 'find');
sandbox.spy(Chat, 'deleteMany');
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await party.trimChat();
expect(Chat.find).to.be.calledOnce;
expect(Chat.deleteMany).to.not.be.called;
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(3);
});
it('Deletes messages over the limit', async () => {
sandbox.spy(Chat, 'find');
sandbox.spy(Chat, 'deleteMany');
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
await party.trimChat(1);
expect(Chat.find).to.be.calledOnce;
expect(Chat.deleteMany).to.be.calledOnce;
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(1);
});
});
describe('#startQuest', () => {
context('Failure Conditions', () => {
it('throws an error if group is not a party', async () => {
@@ -1,19 +0,0 @@
import {
generateUser,
requester,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /analytics/track/:eventName', () => {
it('calls res.analytics', async () => {
const user = await generateUser();
sandbox.spy(analytics, 'track');
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
sandbox.restore();
});
});
@@ -91,6 +91,23 @@ describe('POST /groups/:groupId/quests/accept', () => {
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
});
it('heals stuck RSVPNeeded when group already has the user accepted', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
await partyMembers[0].sync();
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
expect(res).to.exist;
await partyMembers[0].sync();
await questingGroup.sync();
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
});
it('does not accept invite for a quest already underway', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
@@ -193,6 +193,23 @@ describe('POST /groups/:groupId/quests/force-start', () => {
expect(questingGroup.quest.members[notInPartyUser._id]).to.not.exist;
});
it('removes users who have been deleted from quest.members', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[0].del('/user', {
password: 'password',
});
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await sleep(0.5);
await questingGroup.sync();
expect(questingGroup.quest.members[partyMembers[0]._id]).to.not.exist;
});
it('removes users who don\'t have true value in quest.members from quest.members', async () => {
const partyMemberThatRejects = partyMembers[1];
const partyMemberThatIgnores = partyMembers[2];
@@ -100,6 +100,23 @@ describe('POST /groups/:groupId/quests/reject', () => {
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
});
it('heals stuck RSVPNeeded when group already has the user rejected', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
await partyMembers[0].sync();
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
expect(res).to.exist;
await partyMembers[0].sync();
await questingGroup.sync();
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(false);
});
it('return an error when a user rejects an invite already accepted', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
@@ -1,7 +1,13 @@
import {
each,
map,
} from 'lodash';
import {
checkExistence,
createAndPopulateGroup,
generateGroup,
generateUser,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
@@ -9,7 +15,6 @@ import {
sha1Encrypt as sha1EncryptPassword,
} from '../../../../../website/server/libs/password';
import * as email from '../../../../../website/server/libs/email';
import sendJob from '../../../../../website/server/libs/worker';
const DELETE_CONFIRMATION = 'DELETE';
@@ -42,13 +47,12 @@ describe('DELETE /user', () => {
});
});
it('sends deletion job to worker', async () => {
const workerStub = sandbox.stub(sendJob, 'sendJob');
it('deletes the user', async () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
await user.del('/user', {
password,
});
expect(workerStub).to.be.calledOnce;
workerStub.restore();
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
it('returns an error if excessive feedback is supplied', async () => {
@@ -80,6 +84,53 @@ describe('DELETE /user', () => {
});
});
it('deletes the user\'s tasks', async () => {
await user.post('/tasks/user', {
text: 'test habit',
type: 'habit',
});
await user.sync();
// gets the user's tasks ids
const ids = [];
each(user.tasksOrder, idsForOrder => {
ids.push(...idsForOrder);
});
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
await user.del('/user', {
password,
});
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
});
it('reduces memberCount in challenges user is linked to', async () => {
const populatedGroup = await createAndPopulateGroup({
members: 2,
});
const { group } = populatedGroup;
const authorizedUser = populatedGroup.members[1];
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
await authorizedUser.post(`/challenges/${challenge._id}/join`);
await challenge.sync();
expect(challenge.memberCount).to.eql(2);
await authorizedUser.del('/user', {
password,
});
await challenge.sync();
expect(challenge.memberCount).to.eql(1);
});
it('sends feedback to the admin email', async () => {
sandbox.spy(email, 'sendTxn');
@@ -107,10 +158,10 @@ describe('DELETE /user', () => {
});
it('deletes the user with a legacy sha1 password', async () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
const textPassword = 'mySecretPassword';
const salt = sha1MakeSalt();
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
const workerStub = sandbox.stub(sendJob, 'sendJob');
await user.updateOne({
'auth.local.hashed_password': sha1HashedPassword,
@@ -128,8 +179,7 @@ describe('DELETE /user', () => {
await user.del('/user', {
password: textPassword,
});
expect(workerStub).to.be.calledOnce;
workerStub.restore();
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
context('last member of a party', () => {
@@ -163,12 +213,11 @@ describe('DELETE /user', () => {
});
it('deletes a Google user', async () => {
const workerStub = sandbox.stub(sendJob, 'sendJob');
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
expect(workerStub).to.be.calledOnce;
workerStub.restore();
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
@@ -183,13 +232,12 @@ describe('DELETE /user', () => {
});
});
it('deletes an Apple user', async () => {
const workerStub = sandbox.stub(sendJob, 'sendJob');
it('deletes a Apple user', async () => {
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
expect(workerStub).to.be.calledOnce;
workerStub.restore();
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
});
@@ -1,7 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /user/sleep', () => {
let user;
@@ -23,15 +22,4 @@ describe('POST /user/sleep', () => {
await user.sync();
expect(user.preferences.sleep).to.be.false;
});
it('sends sleep status to analytics service', async () => {
sandbox.spy(analytics, 'track');
await user.post('/user/sleep');
await user.sync();
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
sandbox.restore();
});
});
@@ -9,6 +9,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { encrypt } from '../../../../../../website/server/libs/encryption';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
@@ -41,6 +42,25 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('tracks a registration event', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
const password = 'password';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
});
it('registers a new user and sets verifiedUsername to true', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
@@ -7,6 +7,7 @@ import {
getProperty,
} from '../../../../../helpers/api-integration/v3';
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
describe('POST /user/auth/social', () => {
let api;
@@ -65,6 +66,19 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('tracks a registration event', async () => {
const socialUser = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', socialUser.id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -231,6 +245,17 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false;
});
it('does not track a registration event for existing users', async () => {
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
await user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+1 -10
View File
@@ -13,7 +13,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buy', () => {
let user;
const analytics = { track () {} };
beforeEach(() => {
user = generateUser({
@@ -32,12 +31,6 @@ describe('shared.ops.buy', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('returns error when key is not provided', async () => {
@@ -51,10 +44,8 @@ describe('shared.ops.buy', () => {
it('buys health potion', async () => {
user.stats.hp = 30;
await buy(user, { params: { key: 'potion' } }, analytics);
await buy(user, { params: { key: 'potion' } });
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('adds equipment to inventory', async () => {
+3 -7
View File
@@ -29,10 +29,9 @@ describe('shared.ops.buyArmoire', () => {
const YIELD_EQUIPMENT = 0.5;
const YIELD_FOOD = 0.7;
const YIELD_EXP = 0.9;
const analytics = { track () {} };
async function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
async function buyArmoire (_user, _req) {
const buyOp = new BuyArmoireOperation(_user, _req);
return buyOp.purchase();
}
@@ -50,12 +49,10 @@ describe('shared.ops.buyArmoire', () => {
user.items.food = {};
sandbox.stub(randomValFns, 'trueRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
randomValFns.trueRandom.restore();
analytics.track.restore();
});
context('failure conditions', () => {
@@ -147,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
expect(_.size(user.items.gear.owned)).to.equal(2);
await buyArmoire(user, {}, analytics);
await buyArmoire(user, {});
expect(_.size(user.items.gear.owned)).to.equal(3);
@@ -155,7 +152,6 @@ describe('shared.ops.buyArmoire', () => {
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100);
expect(analytics.track).to.be.calledTwice;
});
});
});
+3 -12
View File
@@ -1,6 +1,5 @@
/* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
@@ -11,15 +10,14 @@ import i18n from '../../../../website/common/script/i18n';
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
async function buyGem (user, req, analytics) {
const buyOp = new BuyGemOperation(user, req, analytics);
async function buyGem (user, req) {
const buyOp = new BuyGemOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyGem', () => {
let user;
const analytics = { track () {} };
const goldPoints = 40;
const gemsBought = 40;
const userGemAmount = 10;
@@ -35,23 +33,16 @@ describe('shared.ops.buyGem', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Gems', () => {
it('purchases gems', async () => {
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', async () => {
+3 -10
View File
@@ -10,10 +10,9 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyHealthPotion', () => {
let user;
const analytics = { track () {} };
async function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
async function buyHealthPotion (_user, _req) {
const buyOp = new BuyHealthPotionOperation(_user, _req);
return buyOp.purchase();
}
@@ -32,19 +31,13 @@ describe('shared.ops.buyHealthPotion', () => {
},
stats: { gp: 200 },
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Potion', () => {
it('recovers 15 hp', async () => {
user.stats.hp = 30;
await buyHealthPotion(user, {}, analytics);
await buyHealthPotion(user, {});
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('does not increase hp above 50', async () => {
+5 -9
View File
@@ -13,15 +13,14 @@ import {
import i18n from '../../../../website/common/script/i18n';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
async function buyGear (user, req, analytics) {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
async function buyGear (user, req) {
const buyOp = new BuyMarketGearOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyMarketGear', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -47,14 +46,12 @@ describe('shared.ops.buyMarketGear', () => {
sinon.stub(shared, 'randomVal');
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
sinon.stub(shared.fns, 'predictableRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
shared.randomVal.restore();
shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) {
clock.restore();
@@ -65,7 +62,7 @@ describe('shared.ops.buyMarketGear', () => {
it('adds equipment to inventory', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
@@ -92,13 +89,12 @@ describe('shared.ops.buyMarketGear', () => {
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
});
expect(analytics.track).to.be.calledOnce;
});
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.be.calledOnce;
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
@@ -111,7 +107,7 @@ describe('shared.ops.buyMarketGear', () => {
user.stats.gp = 31;
user.achievements.purchasedEquipment = true;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.not.be.called;
});
+2 -5
View File
@@ -14,7 +14,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -27,11 +26,9 @@ describe('shared.ops.buyMysterySet', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -93,7 +90,7 @@ describe('shared.ops.buyMysterySet', () => {
context('successful purchases', () => {
it('buys Steampunk Accessories Set', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
await buyMysterySet(user, { params: { key: '301404' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
@@ -106,7 +103,7 @@ describe('shared.ops.buyMysterySet', () => {
it('buys mystery set if it is available', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-16'));
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
await buyMysterySet(user, { params: { key: '201601' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
+2 -5
View File
@@ -12,10 +12,9 @@ describe('shared.ops.buyQuestGems', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGemOperation(_user, _req);
return buyOp.purchase();
}
@@ -25,13 +24,11 @@ describe('shared.ops.buyQuestGems', () => {
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sinon.useFakeTimers(new Date('2024-01-16'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
+8 -17
View File
@@ -12,21 +12,15 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyQuest', () => {
let user;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('buys a Quest scroll', async () => {
@@ -35,12 +29,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 1,
});
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
@@ -49,10 +42,9 @@ describe('shared.ops.buyQuest', () => {
user.items.quests[key] = -1;
await buyQuest(user, {
params: { key },
}, analytics);
});
expect(user.items.quests[key]).to.equal(1);
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
@@ -61,13 +53,13 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
await buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: '3',
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 4,
@@ -82,7 +74,7 @@ describe('shared.ops.buyQuest', () => {
key: 'dilatoryDistress1',
},
quantity: 'a',
}, analytics);
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -187,12 +179,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress3',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress3: 1,
});
expect(user.stats.gp).to.equal(100);
expect(analytics.track).to.be.calledOnce;
});
});
+5 -11
View File
@@ -14,20 +14,17 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buySpecialSpell', () => {
let user;
let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
async function buySpecialSpell (_user, _req) {
const buyOp = new BuySpellOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -78,7 +75,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'thankyou',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
@@ -89,7 +86,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('buys a limited card when it is available', async () => {
@@ -101,7 +97,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'nye',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).to.equal(1);
@@ -112,7 +108,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the card is not currently available', async () => {
@@ -140,7 +135,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'seafoam',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).to.equal(1);
@@ -151,7 +146,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the spell is not currently available', async () => {
+3 -10
View File
@@ -13,21 +13,15 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
describe('common.ops.hourglassPurchase', () => {
let user;
const analytics = { track () {} };
async function buyMount (_user, _req, _analytics) {
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
async function buyMount (_user, _req) {
const buyOp = new BuyHourglassMountOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('failure conditions', () => {
@@ -131,12 +125,11 @@ describe('common.ops.hourglassPurchase', () => {
it('buys a pet', async () => {
user.purchased.plan.consecutive.trinkets = 2;
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
expect(message).to.eql(i18n.t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
expect(analytics.track).to.be.calledOnce;
});
it('buys a mount', async () => {
+4 -8
View File
@@ -17,20 +17,17 @@ describe('shared.ops.purchase', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
before(() => {
user = generateUser({ 'stats.class': 'rogue' });
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
@@ -187,11 +184,10 @@ describe('shared.ops.purchase', () => {
const type = 'eggs';
const key = 'Wolf';
await purchase(user, { params: { type, key } }, analytics);
await purchase(user, { params: { type, key } });
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
});
it('purchases hatchingPotions', async () => {
@@ -332,7 +328,7 @@ describe('shared.ops.purchase', () => {
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -345,7 +341,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
await purchase(user, { params: { type, key }, quantity: -2 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -358,7 +354,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
await purchase(user, { params: { type, key }, quantity: 2.9 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
-15
View File
@@ -54,19 +54,4 @@ describe('armoire', () => {
const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384);
});
it('sets have at least 2 items', () => {
const setMap = {};
forEach(armoire.all, item => {
// Gotta have one outlier
if (!item.set || item.set.startsWith('armoire-')) return;
if (setMap[item.set] === undefined) {
setMap[item.set] = 0;
}
setMap[item.set] += 1;
});
Object.keys(setMap).forEach(set => {
expect(setMap[set], set).to.be.at.least(2);
});
});
});
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|| route.indexOf('/paypal') === 0
|| route.indexOf('/amazon') === 0
|| route.indexOf('/stripe') === 0
|| route.indexOf('/analytics') === 0
) {
url += `${route}`;
} else {
-8
View File
@@ -12,20 +12,12 @@ module.exports = {
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// TODO find a way to let eslint understand webpack aliases
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
'import/extensions': 'off',
'prefer-regex-literals': 'warn',
'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
// this creates issues with the current way we have to push the process.env vars to webpack
// https://github.com/eslint/eslint/issues/14918
// https://github.com/webpack/webpack/issues/5392
// off for now, because any eslint --fix will then still do it anyway
// maybe this can be turned on again once we switch to newer vue/vite
// Important! process.env.XYZ should not be destructured
'prefer-destructuring': 'off',
'vue/html-self-closing': ['error', {
html: {
void: 'never',
+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica - FAQ</title>
<meta name="description" content="Frequently Asked Questions about Habitica, the gamified task manager.">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="app"></div>
<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/core' vite-ignore></script>
</body>
</html>
+20 -589
View File
@@ -41,7 +41,6 @@
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
@@ -55,9 +54,7 @@
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
"vitest": "^3.0.5"
}
},
"node_modules/@amplitude/analytics-connector": {
@@ -2111,8 +2108,9 @@
"version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -3634,41 +3632,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -3679,8 +3648,9 @@
"version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -3876,181 +3846,6 @@
"vue-template-compiler": "^2.x"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
}
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
"@webassemblyjs/helper-api-error": "1.13.2",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/wasm-gen": "1.14.1"
}
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
}
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/helper-wasm-section": "1.14.1",
"@webassemblyjs/wasm-gen": "1.14.1",
"@webassemblyjs/wasm-opt": "1.14.1",
"@webassemblyjs/wasm-parser": "1.14.1",
"@webassemblyjs/wast-printer": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/ieee754": "1.13.2",
"@webassemblyjs/leb128": "1.13.2",
"@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
"@webassemblyjs/wasm-gen": "1.14.1",
"@webassemblyjs/wasm-parser": "1.14.1"
}
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-api-error": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
"@webassemblyjs/ieee754": "1.13.2",
"@webassemblyjs/leb128": "1.13.2",
"@webassemblyjs/utf8": "1.13.2"
}
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2"
}
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -4098,48 +3893,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/amplitude-js": {
"version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
@@ -4617,8 +4370,9 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/cac": {
"version": "6.7.14",
@@ -4783,16 +4537,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -4859,8 +4603,9 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -5196,20 +4941,6 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.2.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/enquirer": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@@ -6384,16 +6115,6 @@
"node": ">=0.10.0"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/expect-type": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@@ -6871,13 +6592,6 @@
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -6921,13 +6635,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"dev": true,
"license": "ISC"
},
"node_modules/habitica-markdown": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
@@ -7726,37 +7433,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-worker": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
"engines": {
"node": ">= 10.13.0"
}
},
"node_modules/jest-worker/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -7841,13 +7517,6 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -7911,20 +7580,6 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/loader-runner": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -8128,13 +7783,6 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT"
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -8458,13 +8106,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -9504,63 +9145,6 @@
"node": ">=v12.22.7"
}
},
"node_modules/schema-utils": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/schema-utils/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/schema-utils/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/secure-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
@@ -9838,8 +9422,9 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -10130,20 +9715,6 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar-mini": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
@@ -10154,8 +9725,9 @@
"version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"devOptional": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -10169,47 +9741,13 @@
"node": ">=10"
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"esbuild": {
"optional": true
},
"uglify-js": {
"optional": true
}
}
},
"node_modules/terser/node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -10585,8 +10123,9 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"devOptional": true,
"license": "MIT"
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
@@ -10965,15 +10504,6 @@
"node": ">=6.0.0"
}
},
"node_modules/vue-fragment": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A==",
"license": "MIT",
"peerDependencies": {
"vue": "^2.5.16"
}
},
"node_modules/vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
@@ -11041,20 +10571,6 @@
"node": ">=18"
}
},
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -11065,91 +10581,6 @@
"node": ">=12"
}
},
"node_modules/webpack": {
"version": "5.102.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
"browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.11",
"json-parse-even-better-errors": "^2.3.1",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^4.3.3",
"tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.4",
"webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependenciesMeta": {
"webpack-cli": {
"optional": true
}
}
},
"node_modules/webpack-sources": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack/node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/webpack/node_modules/acorn-import-phases": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
},
"peerDependencies": {
"acorn": "^8.14.0"
}
},
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+1 -4
View File
@@ -46,7 +46,6 @@
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
@@ -60,8 +59,6 @@
"jsdom": "^26.0.0",
"mocha": "^11.1.0",
"playwright": "^1.50.1",
"terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
"vitest": "^3.0.5"
}
}
+134 -2
View File
@@ -1,3 +1,68 @@
.quest_lostMasterclasser4 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_windup {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_solarSystem {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_virtualpet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
width: 219px;
height: 219px;
}
.quest_alien {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
width: 219px;
height: 219px;
}
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
.Pet_HatchingPotion_Alien {
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Dessert {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
}
.Pet_HatchingPotion_Veggie {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
}
.Pet_HatchingPotion_Windup {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
}
.Pet_HatchingPotion_VirtualPet {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
}
.Pet_HatchingPotion_Fungi {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
}
.Pet_HatchingPotion_Cryptid {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
}
.Pet_HatchingPotion_Alien {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
}
.Gems {
display:inline-block;
margin-right:5px;
@@ -26,7 +91,6 @@
margin-left: -3px;
margin-top: -18px;
}
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
width: 90px;
height: 90px;
@@ -34,6 +98,7 @@
/* Critical */
.weapon_special_critical {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
width: 90px;
height: 90px;
margin-left:-12px;
@@ -44,7 +109,6 @@
.weapon_special_1 {
margin-left: -12px;
}
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
width: 90px;
height: 90px;
@@ -53,15 +117,36 @@
.back_special_heroicAureole {
width: 114px;
height: 90px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
}
.head_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
}
.head_special_1 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
margin-top: 3px;
}
.broad_armor_special_0,.slim_armor_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
}
.broad_armor_special_1,.slim_armor_special_1 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
}
.shield_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
}
.weapon_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
}
.Pet-Wolf-Cerberus {
width: 105px;
height: 72px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
@@ -69,17 +154,36 @@
height: 120px;
}
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
}
.eyewear_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
}
.head_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
}
.shield_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
}
.weapon_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
width: 120px;
height: 120px;
}
.Pet-Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
width: 81px;
height: 99px;
}
.Pet-Gryphatrice-Jubilant {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
width: 81px;
height: 96px;
}
@@ -89,11 +193,39 @@
height: 135px;
}
.Mount_Head_Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
}
.Mount_Body_Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
}
.Mount_Head_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
}
.Mount_Body_Dragon-Hydra {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
}
.background_airship, .background_clocktower, .background_steamworks {
width: 141px;
height: 147px;
}
.background_airship {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
}
.background_clocktower {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
}
.background_steamworks {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
}
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */
@@ -695,6 +695,11 @@
width: 141px;
height: 147px;
}
.background_beach_with_volcano {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beach_with_volcano.png');
width: 141px;
height: 147px;
}
.background_beehive {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beehive.png');
width: 141px;
@@ -2346,6 +2351,11 @@
width: 141px;
height: 147px;
}
.background_tropical_coral_garden {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tropical_coral_garden.png');
width: 141px;
height: 147px;
}
.background_tulip_garden {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tulip_garden.png');
width: 141px;
@@ -2401,6 +2411,11 @@
width: 141px;
height: 147px;
}
.background_vegetable_garden {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_vegetable_garden.png');
width: 141px;
height: 147px;
}
.background_viking_ship {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_viking_ship.png');
width: 141px;
@@ -29880,6 +29895,11 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_kendoBogu {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_kendoBogu.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_lamplightersGreatcoat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lamplightersGreatcoat.png');
width: 114px;
@@ -30535,6 +30555,11 @@
width: 90px;
height: 90px;
}
.head_armoire_kendoMen {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_kendoMen.png');
width: 114px;
height: 90px;
}
.head_armoire_lamplightersTopHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lamplightersTopHat.png');
width: 114px;
@@ -30920,6 +30945,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_gardenHose {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenHose.png');
width: 114px;
height: 90px;
}
.shield_armoire_gardenersSpade {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png');
width: 114px;
@@ -31550,6 +31580,11 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_kendoBogu {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_kendoBogu.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_lamplightersGreatcoat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lamplightersGreatcoat.png');
width: 114px;
@@ -31930,6 +31965,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_brightRainbowKite {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_brightRainbowKite.png');
width: 114px;
height: 90px;
}
.weapon_armoire_buoyantBubbles {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_buoyantBubbles.png');
width: 114px;
@@ -32030,6 +32070,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_gardenRake {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenRake.png');
width: 114px;
height: 90px;
}
.weapon_armoire_gardenersWateringCan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png');
width: 114px;
@@ -32125,6 +32170,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_kendoShinai {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_kendoShinai.png');
width: 114px;
height: 90px;
}
.weapon_armoire_lamplighter {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_lamplighter.png');
width: 114px;
@@ -32210,6 +32260,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_pastelRainbowKite {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pastelRainbowKite.png');
width: 114px;
height: 90px;
}
.weapon_armoire_pinkKite {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pinkKite.png');
width: 114px;
@@ -34200,6 +34255,11 @@
width: 114px;
height: 90px;
}
.eyewear_mystery_202606 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202606.png');
width: 117px;
height: 120px;
}
.head_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
width: 114px;
@@ -34220,11 +34280,31 @@
width: 114px;
height: 90px;
}
.head_mystery_202606 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202606.png');
width: 117px;
height: 120px;
}
.shield_mystery_202605 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
width: 114px;
height: 90px;
}
.shield_mystery_202606 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202606.png');
width: 117px;
height: 120px;
}
.shield_mystery_202607 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202607.png');
width: 117px;
height: 120px;
}
.shield_mystery_202608 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202608.png');
width: 117px;
height: 120px;
}
.slim_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
width: 114px;
@@ -34250,6 +34330,16 @@
width: 114px;
height: 90px;
}
.weapon_mystery_202607 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202607.png');
width: 117px;
height: 120px;
}
.weapon_mystery_202608 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202608.png');
width: 117px;
height: 120px;
}
.back_mystery_201402 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
width: 90px;
@@ -37715,6 +37805,26 @@
width: 114px;
height: 90px;
}
.broad_armor_special_summer2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_summer2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Mage.png');
width: 114px;
height: 117px;
}
.broad_armor_special_summer2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Rogue.png');
width: 114px;
height: 117px;
}
.broad_armor_special_summer2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png');
width: 90px;
@@ -37965,6 +38075,26 @@
width: 114px;
height: 90px;
}
.head_special_summer2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Healer.png');
width: 114px;
height: 90px;
}
.head_special_summer2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Mage.png');
width: 114px;
height: 117px;
}
.head_special_summer2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Rogue.png');
width: 114px;
height: 117px;
}
.head_special_summer2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Warrior.png');
width: 114px;
height: 90px;
}
.head_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png');
width: 90px;
@@ -38155,6 +38285,21 @@
width: 114px;
height: 90px;
}
.shield_special_summer2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Healer.png');
width: 114px;
height: 90px;
}
.shield_special_summer2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Rogue.png');
width: 114px;
height: 117px;
}
.shield_special_summer2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png');
width: 90px;
@@ -38395,6 +38540,26 @@
width: 114px;
height: 90px;
}
.slim_armor_special_summer2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_summer2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Mage.png');
width: 114px;
height: 117px;
}
.slim_armor_special_summer2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Rogue.png');
width: 114px;
height: 117px;
}
.slim_armor_special_summer2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png');
width: 90px;
@@ -38635,6 +38800,26 @@
width: 114px;
height: 90px;
}
.weapon_special_summer2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_summer2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Mage.png');
width: 114px;
height: 117px;
}
.weapon_special_summer2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Rogue.png');
width: 114px;
height: 117px;
}
.weapon_special_summer2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png');
width: 90px;
+1
View File
@@ -42,6 +42,7 @@ ul {
font-weight: 400;
line-height: 1.75;
color: $purple-200;
cursor: pointer;
}
h4 {
+4
View File
@@ -16,6 +16,10 @@
border-bottom: 0;
}
.d-content {
display: contents;
}
* {
transition: none;
}
+10 -1
View File
@@ -11,7 +11,7 @@
{{ $t('tavernDiscontinued') }}
</h1>
<p>{{ $t('tavernDiscontinuedDetail') }}</p>
<p v-html="$t('tavernDiscontinuedLinks')"></p>
<p v-html="$t('tavernDiscontinuedLinks', tavernLinks)"></p>
</div>
<div v-else>
<h1>
@@ -38,6 +38,15 @@
import { mapState } from '@/libs/store';
export default {
data () {
return {
tavernLinks: {
faqLink: '<a href="/static/faq/tavern-and-guilds">',
homeLink: '<a href="/">',
linkClose: '</a>',
},
};
},
computed: {
...mapState(['isUserLoggedIn']),
retiredChatPage () {
@@ -98,7 +98,7 @@
<div
v-once
class="opt-out-description"
v-html="$t('optOutOfClassesText')"
v-html="$t('optOutOfClassesText', optOutLinks)"
></div>
</div>
</div>
@@ -214,6 +214,10 @@ export default {
wizard: wizardIcon,
}),
selectedClass: 'warrior',
optOutLinks: {
linkOpen: '<a href="/static/faq#what-classes" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
@@ -14,6 +14,7 @@
<br>
<a
:href="$t('conRewardsURL')"
rel="noopener noreferrer"
target="_blank"
>{{ $t('contribLink') }}</a>
<br>
@@ -55,7 +55,7 @@
</button>
<h4
class="text-center"
v-html="$t('dyingOftenTips')"
v-html="$t('dyingOftenTips', tipLinks)"
></h4>
</div>
</div>
@@ -86,6 +86,10 @@ export default {
data () {
return {
maxHealth,
tipLinks: {
linkOpen: '<a href="/static/faq#prevent-damage" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
@@ -52,7 +52,7 @@
&& user.achievements.ultimateGearSets.rogue
&& user.achievements.ultimateGearSets.warrior"
>
<p v-html="$t('moreGearAchievements')"></p>
<p v-html="$t('moreGearAchievements', gearAchievementLinks)"></p>
<br>
</div>
<Sprite image-name="shop_armoire" />
@@ -95,6 +95,14 @@ export default {
achievementAvatar,
Sprite,
},
data () {
return {
gearAchievementLinks: {
linkOpen: '<a href="/user/settings/site" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
...mapState({ user: 'user.data' }),
},
@@ -108,15 +108,15 @@ export default {
const allEmails = [];
if (user.auth.local.email) allEmails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.emails) {
const emails = user.auth.google.emails;
const { emails } = user.auth.google;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.apple && user.auth.apple.emails) {
const emails = user.auth.apple.emails;
const { emails } = user.auth.apple;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.facebook && user.auth.facebook.emails) {
const emails = user.auth.facebook.emails;
const { emails } = user.auth.facebook;
allEmails.push(...this.findSocialEmails(emails));
}
return allEmails;
@@ -491,6 +491,7 @@
v-if="hero.purchased.plan.paymentMethod === 'Google'"
class="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
:href="playOrdersUrl"
>
Play Console
@@ -499,6 +500,7 @@
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
class="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
>
PayPal Dashboard
@@ -507,6 +509,7 @@
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary btn-sm"
target="_blank"
rel="noopener noreferrer"
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
>
Stripe Dashboard
@@ -609,7 +612,7 @@ 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 { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
const humanReadablePaymentDetails = {
customerId: {
@@ -11,6 +11,7 @@
<li>
<a
href="https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8"
rel="noopener noreferrer"
target="_blank"
>{{ $t('mobileIOS') }}
</a>
@@ -18,6 +19,7 @@
<li>
<a
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
rel="noopener noreferrer"
target="_blank"
>{{ $t('mobileAndroid') }}
</a>
@@ -52,6 +54,7 @@
<a
href="https://habitica.wordpress.com/"
target="_blank"
rel="noopener noreferrer"
>{{ $t('companyBlog') }}
</a>
</li>
@@ -71,6 +74,7 @@
<li>
<a
target="_blank"
rel="noopener noreferrer"
href="/static/community-guidelines"
>{{ $t('communityGuidelines') }}
</a>
@@ -84,6 +88,7 @@
<a
href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica"
target="_blank"
rel="noopener noreferrer"
>{{ $t('companyContribute') }}
</a>
</li>
@@ -91,6 +96,7 @@
<a
href="https://translate.habitica.com/"
target="_blank"
rel="noopener noreferrer"
>{{ $t('translateHabitica') }}
</a>
</li>
@@ -111,6 +117,7 @@
<a
href=""
target="_blank"
rel="noopener noreferrer"
@click.prevent="openBugReportModal()"
>
{{ $t('reportBug') }}
@@ -122,6 +129,7 @@
<a
href="mailto:admin@habitica.com?subject=Habitica Web Bug Report"
target="_blank"
rel="noopener noreferrer"
>
{{ $t('reportBug') }}
</a>
@@ -130,6 +138,7 @@
<a
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
target="_blank"
rel="noopener noreferrer"
>{{ $t('requestFeature') }}
</a>
</li>
@@ -143,6 +152,7 @@
<a
href="/apidoc"
target="_blank"
rel="noopener noreferrer"
>{{ $t('APIv3') }}
</a>
</li>
@@ -150,6 +160,7 @@
<a
:href="getDataDisplayToolUrl"
target="_blank"
rel="noopener noreferrer"
>{{ $t('dataDisplayTool') }}
</a>
</li>
@@ -157,6 +168,7 @@
<a
href="https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths"
target="_blank"
rel="noopener noreferrer"
>{{ $t('guidanceForBlacksmiths') }}
</a>
</li>
@@ -172,6 +184,7 @@
class="social-circle mr-2"
href="https://www.instagram.com/habitica/"
target="_blank"
rel="noopener noreferrer"
>
<div
class="social-icon svg-icon"
@@ -181,6 +194,7 @@
<a
href="https://www.instagram.com/habitica/"
target="_blank"
rel="noopener noreferrer"
>
{{ $t('communityInstagram') }}
</a>
@@ -190,6 +204,7 @@
class="social-circle mr-2"
href="https://bsky.app/profile/habitica.com"
target="_blank"
rel="noopener noreferrer"
>
<div
class="social-icon svg-icon"
@@ -199,6 +214,7 @@
<a
href="https://bsky.app/profile/habitica.com"
target="_blank"
rel="noopener noreferrer"
>
Bluesky
</a>
@@ -208,6 +224,7 @@
class="social-circle mr-2"
href="https://www.facebook.com/Habitica/"
target="_blank"
rel="noopener noreferrer"
>
<div
class="social-icon svg-icon"
@@ -217,6 +234,7 @@
<a
href="https://www.facebook.com/Habitica/"
target="_blank"
rel="noopener noreferrer"
>
{{ $t('communityFacebook') }}
</a>
@@ -226,6 +244,7 @@
class="social-circle mr-2"
href="http://blog.habitrpg.com/"
target="_blank"
rel="noopener noreferrer"
>
<div
class="social-icon svg-icon"
@@ -235,6 +254,7 @@
<a
href="http://blog.habitrpg.com/"
target="_blank"
rel="noopener noreferrer"
>
{{ $t('tumblr') }}
</a>
@@ -266,12 +286,14 @@
<span class="privacy-policy">
<a
target="_blank"
rel="noopener noreferrer"
href="/static/privacy"
>{{ $t('privacy') }}</a>
</span>
<span class="terms">
<a
target="_blank"
rel="noopener noreferrer"
href="/static/terms"
>{{ $t('terms') }}</a>
</span>
@@ -287,12 +309,14 @@
<div class="privacy-policy mx-auto mb-2">
<a
target="_blank"
rel="noopener noreferrer"
href="/static/privacy"
>{{ $t('privacy') }}</a>
</div>
<div class="mobile-terms mx-auto mb-2">
<a
target="_blank"
rel="noopener noreferrer"
href="/static/terms"
>{{ $t('terms') }}</a>
</div>
@@ -20,6 +20,29 @@
class="form mx-auto"
@submit.prevent.stop="register()"
>
<div v-if="needsEmailField">
<input
id="emailInput"
v-model="email"
class="form-control dark"
type="text"
:placeholder="$t('emailAddress')"
:class="{
'mb-3': !emailError,
'input-invalid input-with-error mb-2': emailError,
'input-valid': email && emailValid,
}"
>
<div
v-if="emailError"
class="input-error"
>
{{ emailError }}
</div>
<p class="purple-600 mb-3">
{{ $t('emailRequiredForSupport') }}
</p>
</div>
<input
id="usernameInput"
v-model="username"
@@ -54,12 +77,13 @@
v-once
class="custom-control-label purple-600"
for="privacyTOS"
v-html="$t('acceptPrivacyTOS')"
v-html="$t('acceptPrivacyTOS', acceptLinks)"
></label>
</div>
<button
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
:disabled="!username || usernameInvalid || !privacyAccepted"
class="btn btn-info d-flex justify-content-center
align-items-center w-100 sign-up mx-auto mb-5"
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
type="submit"
>
{{ $t('getStarted') }}
@@ -133,10 +157,12 @@
border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
&:focus, &:active {
background-color: $blue-50;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&:not(:disabled):not(.disabled) {
&:focus, &:active {
background-color: $blue-50;
border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
}
}
}
@@ -148,23 +174,24 @@
<script>
import debounce from 'lodash/debounce';
import PrivacyBanner from '@/components/header/banners/privacy';
import accountCreation from '@/mixins/accountCreation';
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
export default {
components: {
PrivacyBanner,
},
mixins: [sanitizeRedirect],
mixins: [accountCreation, sanitizeRedirect],
data () {
return {
authData: {},
email: '',
password: '',
passwordConfirm: '',
privacyAccepted: false,
registrationMethod: null,
username: '',
usernameIssues: [],
needsEmailField: false,
acceptLinks: {
termsLink: '<a href="/static/terms" target="_blank" rel="noreferrer noopener">',
privacyLink: '<a href="/static/privacy" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
@@ -183,30 +210,40 @@ export default {
},
},
mounted () {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
this.authData = this.$store.state.registrationOptions.authData;
this.email = this.$store.state.registrationOptions.email;
this.username = this.$store.state.registrationOptions.username;
this.password = this.$store.state.registrationOptions.password;
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
if (!this.email) {
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
if (!this.email) {
this.email = window.sessionStorage.getItem('apple-email');
}
} else if (!this.$store.state.registrationOptions.registrationMethod) {
this.$router.push('/');
} else {
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
}
if (!this.email && this.registrationMethod !== 'apple') {
return;
}
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
}).then(res => {
if (!res.issues) {
this.username = usernameToCheck;
}
});
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
this.needsEmailField = true;
}
if (this.email) {
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck,
}).then(res => {
if (!res.issues) {
this.username = usernameToCheck;
}
});
}
document.getElementById('usernameInput').focus();
},
methods: {
@@ -237,6 +274,7 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'),
username: this.username,
email: this.email,
allowRegister: true,
});
} else {
@@ -64,7 +64,7 @@
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
v-html="$t('visitCustomizationsShop', customizeLinks)"
></p>
</div>
<customize-banner
@@ -104,6 +104,10 @@ export default {
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
},
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
customizeLinks: {
linkOpen: '<a href="/shops/customizations">',
linkClose: '</a>',
},
};
},
computed: {
@@ -61,7 +61,7 @@
<p
v-once
class="w-50 mx-auto"
v-html="$t('visitCustomizationsShop')"
v-html="$t('visitCustomizationsShop', customizeLinks)"
></p>
</div>
</div>
@@ -95,6 +95,14 @@ export default {
props: [
'editing',
],
data () {
return {
customizeLinks: {
linkOpen: '<a href="/shops/customizations">',
linkClose: '</a>',
},
};
},
computed: {
hairSubMenuItems () {
const items = [
@@ -217,8 +217,8 @@ export default {
mixins: [notifications, userStateMixin],
data () {
const abuseFlagModalBody = {
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
secondLinkStart: '<a href="/static/terms" target="_blank">',
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
linkEnd: '</a>',
};
@@ -189,7 +189,7 @@ export default {
this.cancel();
return [];
}
this.currentSearch = regexRes[1];
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
if (this.currentSearch.length === 0) return [];
@@ -208,8 +208,8 @@ export default {
mixins: [notifications, userStateMixin],
data () {
const abuseFlagModalBody = {
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
secondLinkStart: '<a href="/static/terms" target="_blank">',
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
linkEnd: '</a>',
};
@@ -470,7 +470,7 @@ export default {
return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false;
const purchased = group.purchased;
const { purchased } = group;
if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false;
@@ -492,7 +492,7 @@ export default {
},
isPartyPreviouslyUpgraded () {
if (!this.userParty) return false;
const purchased = this.userParty.purchased;
const { purchased } = this.userParty;
if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
@@ -533,7 +533,7 @@ export default {
this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) {
this.selectedOption = this.upgradeableGuilds[0];
[this.selectedOption] = this.upgradeableGuilds;
} else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty;
} else {
@@ -77,7 +77,7 @@
</ul>
<div
class="mx-auto"
v-html="$t('newGroupsVisitFAQ')"
v-html="$t('newGroupsVisitFAQ', faqLinks)"
></div>
<div
class="mx-auto"
@@ -193,6 +193,10 @@ export default {
close: closeIcon,
sparkles,
}),
faqLinks: {
linkOpen: '<a href="/static/faq#group-plans" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
methods: {
@@ -198,7 +198,6 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
@@ -438,14 +437,6 @@ export default {
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
@@ -6,7 +6,7 @@
<div
v-once
class="col col-sm-12 col-xl-8"
v-html="$t('communityGuidelinesIntro')"
v-html="$t('communityGuidelinesIntro', guidelineLinks)"
></div>
<div class="col-md-auto col-md-12 col-xl-4">
<button
@@ -60,6 +60,14 @@
import { mapState } from '@/libs/store';
export default {
data () {
return {
guidelineLinks: {
linkOpen: '<a href="/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
...mapState({ user: 'user.data' }),
communityGuidelinesAccepted () {
@@ -240,7 +240,6 @@
<script>
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
@@ -276,11 +275,6 @@ export default {
this.$store.state.party.data = party;
this.user.party._id = party._id;
Analytics.updateUser({
partyID: party._id,
partySize: 1,
});
this.$root.$emit('bv::hide::modal', 'create-party-modal');
await this.$router.push('/party');
},
@@ -314,7 +314,6 @@ import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupGemsModal from '@/components/groups/groupGemsModal';
@@ -560,7 +559,6 @@ export default {
if (this.isParty) {
data.type = 'party';
Analytics.updateUser({ partySize: null, partyID: null });
this.$store.state.partyMembers = [];
}
@@ -74,7 +74,7 @@
class="no-quest-to-start"
>
<b>{{ $t('noQuestToStartTitle') }}</b> <br>
<span v-html="$t('noQuestToStart', { questShop: '/shops/quests' })"></span>
<span v-html="$t('noQuestToStart', questLinks)"></span>
</span>
</div>
</div>
@@ -334,7 +334,6 @@ import orderBy from 'lodash/orderBy';
import * as quests from '@/../../common/script/content/quests';
import getItemInfo from '@/../../common/script/libs/getItemInfo';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
import questDialogContent from '../shops/quests/questDialogContent';
@@ -370,6 +369,10 @@ export default {
shareUserIdShown: false,
quests,
sortBy: 'AZ',
questLinks: {
linkOpen: '<a href="/shops/quests">',
linkClose: '</a>',
},
};
},
computed: {
@@ -421,11 +424,6 @@ export default {
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest;
@@ -1,889 +0,0 @@
<template>
<div class="row">
<world-boss-info-modal />
<world-boss-rage-modal />
<div class="col-12 col-sm-8 clearfix standard-page">
<div class="row">
<div class="col-6 title-details">
<h1 v-once>
{{ $t('welcomeToTavern') }}
</h1>
</div>
</div>
<chat
:label="$t('tavernChat')"
:group="group"
:placeholder="$t('tavernCommunityGuidelinesPlaceholder')"
@fetchRecentMessages="fetchRecentMessages()"
/>
</div>
<div class="col-12 col-sm-4 sidebar">
<div class="section">
<div
class="grassy-meadow-backdrop"
:style="{'background-image': imageURLs.background}"
>
<div
class="daniel_front"
:style="{'background-image': imageURLs.npc}"
></div>
</div>
<div class="boss-section">
<div
v-if="group && group.quest && group.quest.active"
class="world-boss"
:style="{
background: questData.colors.dark,
'border-color': questData.colors.extralight,
'outline-color': questData.colors.light}"
>
<div
class="corner-decoration"
:style="{top: '-2px', right: '-2px'}"
></div>
<div
class="corner-decoration"
:style="{top: '-2px', left: '-2px'}"
></div>
<div
class="corner-decoration"
:style="{bottom: '-2px', right: '-2px'}"
></div>
<div
class="corner-decoration"
:style="{bottom: '-2px', left: '-2px'}"
></div>
<div class="text-center float-bar d-flex align-items-center">
<span class="diamond"></span>
<span
class="strong reduce"
:style="{background: questData.colors.dark}"
>{{ $t('worldBossEvent') }}</span>
<span class="diamond"></span>
</div>
<div class="boss-gradient pb-3 pt-3">
<p
class="text-center reduce"
:style="{color: questData.colors.extralight}"
>
{{ $t(`${questData.key}ArtCredit`) }}
</p>
<div
class="quest-boss"
:class="'background_' + questData.key"
>
<div
class="quest-boss"
:class="'quest_' + questData.key"
></div>
<div
class="quest-boss"
:class="'phobia_' + questData.key"
:style="{display: 'none'}"
></div>
</div>
</div>
<div class="p-3">
<div class="row d-flex align-items-center mb-2">
<div class="col-sm-6">
<strong class="float-left">{{ questData.boss.name() }}</strong>
</div>
<div class="col-sm-6">
<span class="d-flex float-right">
<div
class="svg-icon boss-icon"
v-html="icons.swordIcon"
></div>
<span
class="ml-1 reduce"
:style="{color: questData.colors.extralight}"
>{{ $t('pendingDamage', {damage: pendingDamage()}) }}</span>
</span>
</div>
</div>
<div class="grey-progress-bar mb-1">
<div
class="boss-health-bar"
:style="{width: (group.quest.progress.hp / questData.boss.hp) * 100 + '%'}"
></div>
</div>
<span class="d-flex align-items-center">
<div
class="svg-icon boss-icon"
v-html="icons.healthIcon"
></div>
<span
class="reduce ml-1 pt-1"
>{{ $t('bossHealth', {
currentHealth: bossCurrentHealth(),
maxHealth: questData.boss.hp.toLocaleString()}) }}</span>
</span>
<div class="mt-3 mb-2">
<strong class="mr-1">{{ $t('rageAttack') }}</strong>
<span>{{ questData.boss.rage.title() }}</span>
</div>
<div class="grey-progress-bar mb-1">
<div
class="boss-health-bar rage-bar"
:style="{
width: (group.quest.progress.rage / questData.boss.rage.value) * 100 + '%'}"
></div>
</div>
<span class="d-flex align-items-center">
<div
class="svg-icon boss-icon"
v-html="icons.rageIcon"
></div>
<span
class="reduce ml-1 pt-1"
>{{ $t('bossRage', {
currentRage: bossCurrentRage(),
maxRage: questData.boss.rage.value.toLocaleString()}) }}</span>
</span>
<div class="row d-flex align-items-center mb-2 mt-2">
<div class="col-sm-4 d-flex">
<strong class="mr-2">{{ $t('rageStrikes') }}</strong>
<div
v-b-tooltip.hover.top="questData.boss.rage.description()"
class="svg-icon boss-icon information-icon m-auto"
v-html="icons.informationIcon"
></div>
</div>
<div class="col-sm-8 d-flex align-items-center justify-content-center">
<div
class="m-auto"
@click="showWorldBossRage('seasonalShop')"
>
<img
v-if="!group.quest.extra.worldDmg.seasonalShop"
class="rage-strike"
src="@/assets/images/world-boss/rage_strike@2x.png"
>
<img
v-if="group.quest.extra.worldDmg.seasonalShop"
class="rage-strike-active"
src="@/assets/images/world-boss/rage_strike-seasonalShop@2x.png"
>
</div>
<div
class="m-auto"
@click="showWorldBossRage('market')"
>
<img
v-if="!group.quest.extra.worldDmg.market"
class="rage-strike"
src="@/assets/images/world-boss/rage_strike@2x.png"
>
<img
v-if="group.quest.extra.worldDmg.market"
class="rage-strike-active"
src="@/assets/images/world-boss/rage_strike-market@2x.png"
>
</div>
<div
class="m-auto"
@click="showWorldBossRage('quests')"
>
<img
v-if="!group.quest.extra.worldDmg.quests"
class="rage-strike"
src="@/assets/images/world-boss/rage_strike@2x.png"
>
<img
v-if="group.quest.extra.worldDmg.quests"
class="rage-strike-active"
src="@/assets/images/world-boss/rage_strike-quests@2x.png"
>
</div>
</div>
</div>
<div
class="boss-description p-3"
:style="{'border-color': questData.colors.extralight}"
@click="sections.worldBoss = !sections.worldBoss"
>
<strong class="float-left">{{ $t('worldBossDescription') }}</strong>
<div class="float-right">
<div
v-if="!sections.worldBoss"
class="toggle-down"
>
<div
class="svg-icon boss-icon"
v-html="icons.chevronIcon"
></div>
</div>
<div
v-if="sections.worldBoss"
class="toggle-up"
>
<div
class="svg-icon boss-icon reverse"
v-html="icons.chevronIcon"
></div>
</div>
</div>
</div>
<div
v-if="sections.worldBoss"
class="mt-3"
v-html="questData.notes()"
></div>
</div>
</div>
<!-- .text-center.mt-4.world-boss
-info-button(@click="showWorldBossInfo()") {{$t('whatIsWorldBoss') }}
-->
</div>
<div class="sleep px-4 py-3">
<strong v-once>{{ $t('sleepDescription') }}</strong>
<ul>
<li v-once>
{{ $t('sleepBullet1') }}
</li>
<li v-once>
{{ $t('sleepBullet2') }}
</li>
<li v-once>
{{ $t('sleepBullet3') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
v-once
class="btn btn-secondary pause-button"
@click="toggleSleep()"
>
{{ $t('pauseDailies') }}
</button>
<button
v-if="user.preferences.sleep"
v-once
class="btn btn-secondary pause-button"
@click="toggleSleep()"
>
{{ $t('unpauseDailies') }}
</button>
</div>
</div>
<div class="px-4">
<sidebar-section :title="$t('staff')">
<div class="row">
<div
v-for="user in staff"
:key="user.uuid"
class="col-6 staff"
:class="{
staff: user.type === 'Staff',
moderator: user.type === 'Moderator'}"
>
<div>
<router-link
class="title"
:to="{'name': 'userProfile', 'params': {'userId': user.uuid}}"
>
{{ user.name }}
</router-link>
<div
v-if="user.type === 'Staff'"
class="svg-icon staff-icon"
v-html="icons.tierStaff"
></div>
</div>
</div>
</div>
</sidebar-section>
<sidebar-section :title="$t('helpfulLinks')">
<ul>
<li>
<a href="mailto:admin@habitica.com">
{{ $t('reportCommunityIssues') }}
</a>
</li>
<li>
<router-link
v-once
to="/static/community-guidelines"
>
{{ $t('communityGuidelines') }}
</router-link>
</li>
<li>
<router-link
to="/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601"
>
{{ $t('lookingForGroup') }}
</router-link>
</li>
<li>
<router-link
v-once
to="/static/faq"
>
{{ $t('faq') }}
</router-link>
</li>
<li>
<a
href
:style="glossary-link"
v-html="$t('glossary')"
></a>
</li>
<li>
<a
v-once
href="https://habitica.fandom.com/wiki/Habitica_Wiki"
target="_blank"
>{{ $t('wiki') }}</a>
</li>
<li>
<a
v-once
href="https://tools.habitica.com/"
target="_blank"
>{{ $t('dataDisplayTool') }}</a>
</li>
<li>
<a
href=""
target="_blank"
@click.prevent="openBugReportModal()"
>
{{ $t('reportBug') }}
</a>
</li>
<li>
<a
v-once
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
target="_blank"
>{{ $t('requestFeature') }}</a>
</li>
<li>
<router-link
to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a"
>
{{ $t('askQuestionGuild') }}
</router-link>
</li>
</ul>
</sidebar-section>
<sidebar-section :title="$t('playerTiers')">
<div class="row">
<div class="col-12">
<p v-once>
{{ $t('playerTiersDesc') }}
</p>
<ul class="tier-list">
<li
v-once
class="tier1"
>
{{ $t('tier1') }}
<div
class="svg-icon tier1-icon"
v-html="icons.tier1"
></div>
</li>
<li
v-once
class="tier2"
>
{{ $t('tier2') }}
<div
class="svg-icon tier2-icon"
v-html="icons.tier2"
></div>
</li>
<li
v-once
class="tier3"
>
{{ $t('tier3') }}
<div
class="svg-icon tier3-icon"
v-html="icons.tier3"
></div>
</li>
<li
v-once
class="tier4"
>
{{ $t('tier4') }}
<div
class="svg-icon tier4-icon"
v-html="icons.tier4"
></div>
</li>
<li
v-once
class="tier5"
>
{{ $t('tier5') }}
<div
class="svg-icon tier5-icon"
v-html="icons.tier5"
></div>
</li>
<li
v-once
class="tier6"
>
{{ $t('tier6') }}
<div
class="svg-icon tier6-icon"
v-html="icons.tier6"
></div>
</li>
<li
v-once
class="tier7"
>
{{ $t('tier7') }}
<div
class="svg-icon tier7-icon"
v-html="icons.tier7"
></div>
</li>
<li
v-once
class="moderator"
>
{{ $t('tierModerator') }}
<div
class="svg-icon mod-icon"
v-html="icons.tierMod"
></div>
</li>
<li
v-once
class="staff"
>
{{ $t('tierStaff') }}
<div
class="svg-icon staff-icon"
v-html="icons.tierStaff"
></div>
</li>
<li
v-once
class="npc"
>
{{ $t('tierNPC') }}
<div
class="svg-icon npc-icon"
v-html="icons.tierNPC"
></div>
</li>
</ul>
</div>
</div>
</sidebar-section>
</div>
</div>
</div>
</template>
<style lang='scss' scoped>
@import '@/assets/scss/colors.scss';
h1 {
color: $purple-200;
}
.sidebar {
background-color: $gray-600;
padding: 0em;
}
.pause-button {
background-color: #ffb445 !important;
color: $white;
width: 100%;
}
.grassy-meadow-backdrop {
background-repeat: repeat-x;
width: 100%;
height: 246px;
}
.daniel_front {
height: 246px;
width: 471px;
background-repeat: no-repeat;
margin: 0 auto;
}
.svg-icon {
width: 10px;
display: inline-block;
margin-left: .5em;
}
.tier1-icon, .tier2-icon {
width: 11px;
}
.tier5-icon, .tier6-icon {
width: 8px;
}
.tier7-icon {
width: 12px;
}
.mod-icon {
width: 13px;
}
.npc-icon {
width: 8px;
}
.boss-icon {
width: 16px;
margin-top: .1em;
margin-left: 0;
}
.boss-icon-large {
width: 48px;
}
.staff {
margin-bottom: 1em;
.staff-icon {
width: 11px;
}
.title {
color: #6133b4;
font-weight: bold;
display: inline-block;
}
}
.tier-list {
list-style-type: none;
padding: 0;
width: 98%;
li {
border-radius: 2px;
background-color: #edecee;
border: solid 1px #c3c0c7;
text-align: center;
padding: 1em;
margin-bottom: 1em;
font-weight: bold;
}
.tier1 {
color: #c42870;
}
.tier2 {
color: #b01515;
}
.tier3 {
color: #d70e14;
}
.tier4 {
color: #c24d00;
}
.tier5 {
color: #9e650f;
}
.tier6 {
color: #2b8363;
}
.tier7 {
color: #167e87;
}
.tier8, .moderator {
color: #277eab;
}
.tier9, .staff {
color: #6133b4;
}
.npc {
color: $black;
}
}
.staff .title {
color: #6133b4;
}
.moderator .title {
color: #277eab;
}
.bailey .title {
color: $black;
}
.boss-section {
padding: 1.75em;
}
.world-boss {
color: $white;
border-style: solid;
border-width: 2px;
outline-style: solid;
outline-width: 2px;
margin: 2px;
position: relative;
}
.quest-boss {
margin: 1em auto;
}
.grey-progress-bar {
width: 100%;
height: 15px;
background-color: rgba(255, 255, 255, 0.24);
border-radius: 2px;
}
.boss-health-bar {
width: 80%;
height: 15px;
margin-bottom: .5em;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
background-color: #f74e52;
}
.boss-health-bar.rage-bar {
background-color: #ff944c;
}
.boss-gradient {
background-image: linear-gradient(to bottom, #401f2a, #931f4d);
margin-top: -1.4em;
}
.boss-description {
border-top: 1px solid;
margin-left: -16px;
margin-right: -16px;
padding: .25em 0 0 .25em;
}
.float-bar {
position: relative;
top: -16px;
width: 162px;
height: 28px;
border-radius: 2px;
background-color: inherit;
margin: auto;
}
.corner-decoration {
position: absolute;
width: 6px;
height: 6px;
background-color: inherit;
border: inherit;
outline: inherit;
}
.reverse {
transform: rotate(180deg);
}
.diamond {
margin: auto;
display: inline-block;
width: 6px;
height: 6px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
background-color: #dc4069;
border: solid 2px #931f4d;
}
.reduce {
font-size: 12px;
}
.rage-strike {
max-width: 50px;
height: auto;
}
.rage-strike-active {
max-width: 75px;
height: auto;
cursor: pointer;
}
.world-boss-info-button {
width: 100%;
background-color: $gray-500;
border-radius: 2px;
font-size: 14px;
color: $blue-10;
padding: 1em;
cursor: pointer;
}
</style>
<script>
import find from 'lodash/find';
import { TAVERN_ID } from '@/../../common/script/constants';
import * as quests from '@/../../common/script/content/quests';
import { mapState } from '@/libs/store';
import { goToModForm } from '@/libs/modform';
import worldBossInfoModal from '../world-boss/worldBossInfoModal';
import worldBossRageModal from '../world-boss/worldBossRageModal';
import sidebarSection from '../sidebarSection';
import chat from './chat';
import challengeIcon from '@/assets/svg/challenge.svg?raw';
import chevronIcon from '@/assets/svg/chevron-red.svg?raw';
import gemIcon from '@/assets/svg/gem.svg?raw';
import healthIcon from '@/assets/svg/health.svg?raw';
import informationIconRed from '@/assets/svg/information-red.svg?raw';
import questBackground from '@/assets/svg/quest-background-border.svg?raw';
import rageIcon from '@/assets/svg/rage.svg?raw';
import swordIcon from '@/assets/svg/sword.svg?raw';
import tier1 from '@/assets/svg/tier-1.svg?raw';
import tier2 from '@/assets/svg/tier-2.svg?raw';
import tier3 from '@/assets/svg/tier-3.svg?raw';
import tier4 from '@/assets/svg/tier-4.svg?raw';
import tier5 from '@/assets/svg/tier-5.svg?raw';
import tier6 from '@/assets/svg/tier-6.svg?raw';
import tier7 from '@/assets/svg/tier-7.svg?raw';
import tierMod from '@/assets/svg/tier-mod.svg?raw';
import tierNPC from '@/assets/svg/tier-npc.svg?raw';
import tierStaff from '@/assets/svg/tier-staff.svg?raw';
import staffList from '../../libs/staffList';
import reportBug from '@/mixins/reportBug.js';
export default {
components: {
worldBossInfoModal,
worldBossRageModal,
sidebarSection,
chat,
},
mixins: [reportBug],
data () {
return {
groupId: TAVERN_ID,
icons: Object.freeze({
challengeIcon,
chevronIcon,
gem: gemIcon,
healthIcon,
informationIcon: informationIconRed,
questBackground,
rageIcon,
swordIcon,
tier1,
tier2,
tier3,
tier4,
tier5,
tier6,
tier7,
tierMod,
tierNPC,
tierStaff,
}),
group: {
chat: [],
},
sections: {
worldBoss: true,
},
staff: staffList,
};
},
computed: {
...mapState({
user: 'user.data',
currentEventList: 'worldState.data.currentEventList',
}),
questData () {
if (!this.group.quest) return {};
return quests.quests[this.group.quest.key];
},
imageURLs () {
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
if (!currentEvent) {
return {
background: 'url(/static/npc/normal/tavern_background.png)',
npc: 'url(/static/npc/normal/tavern_npc.png)',
};
}
return {
background: `url(/static/npc/${currentEvent.season}/tavern_background.png)`,
npc: `url(/static/npc/${currentEvent.season}/tavern_npc.png)`,
};
},
},
async mounted () {
this.$store.dispatch('common:setTitle', {
subSection: this.$t('tavern'),
section: this.$t('guilds'),
});
this.group = await this.$store.dispatch('guilds:getGroup', { groupId: TAVERN_ID });
},
methods: {
modForm () {
goToModForm(this.user);
},
toggleSleep () {
this.$store.dispatch('user:sleep');
},
pendingDamage () {
if (!this.user.party.quest.progress.up) return 0;
return this.$options.filters.floor(this.user.party.quest.progress.up, 10);
// keep user's pending damage consistent with how it's displayed on the party page
},
bossCurrentHealth () {
if (!this.group.quest.progress.hp) return 0;
return Math.ceil(parseFloat(this.group.quest.progress.hp)).toLocaleString();
},
bossCurrentRage () {
if (!this.group.quest.progress.hp) return 0;
return Math.floor(parseFloat(this.group.quest.progress.rage)).toLocaleString();
},
showWorldBossInfo () {
this.$root.$emit('bv::show::modal', 'world-boss-info');
},
showWorldBossRage (npc) {
if (this.group.quest.extra.worldDmg[npc]) {
this.$store.state.rageModalOptions.npc = npc;
this.$root.$emit('bv::show::modal', 'world-boss-rage');
}
},
async fetchRecentMessages () {
this.group = await this.$store.dispatch('guilds:getGroup', { groupId: TAVERN_ID });
},
},
};
</script>
@@ -3,7 +3,7 @@
<div class="row standard-page">
<small
class="muted"
v-html="$t('blurbHallContributors')"
v-html="$t('blurbHallContributors', hallLinks)"
></small>
</div>
<div class="row standard-page">
@@ -68,6 +68,7 @@
<a
href="https://habitica.fandom.com/wiki/Contributor_Rewards"
target="_blank"
rel="noopener noreferrer"
>More details</a>
</small>
</div>
@@ -296,6 +297,7 @@
<div
v-markdown="hero.contributor.contributions"
target="_blank"
rel="noopener noreferrer"
></div>
</td>
</tr>
@@ -360,6 +362,12 @@ export default {
expandItems: false,
expandAuth: false,
expandTransactions: false,
hallLinks: {
linkRewards: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tier-rewards" target="_blank" rel="noreferrer noopener">',
linkTiers: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica#contributor-tiers" target="_blank" rel="noreferrer noopener">',
linkContributing: '<a href="https://github.com/HabitRPG/habitica/wiki/Contributing-to-Habitica" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
async mounted () {
@@ -1,64 +0,0 @@
<template>
<base-banner
v-if="showChatWarning"
banner-id="chat-warning"
banner-class="chat-banner"
class="chat-banner"
height="3rem"
:class="{faq: faqPage}"
>
<div
slot="content"
class="w-100 text-center"
v-html="$t('chatSunsetWarning')"
>
</div>
</base-banner>
</template>
<style lang="scss">
@import '@/assets/scss/colors.scss';
.chat-banner {
width: 100%;
min-height: 48px;
padding: 8px;
color: $orange-1;
background-color: $orange-100;
line-height: 1.71;
a {
color: $orange-1;
text-decoration: underline;
&:hover {
color: $orange-1;
}
}
&.faq {
position: fixed;
top: 3.5rem;
}
}
</style>
<script>
import BaseBanner from './base';
export default {
components: {
BaseBanner,
},
computed: {
faqPage () {
return (this.$route.fullPath.indexOf('/faq')) !== -1;
},
showChatWarning () {
return false;
},
},
};
</script>
@@ -6,7 +6,7 @@
>
<p
class="mr-3 mb-0"
v-html="$t('privacyOverview') + ' ' + $t('learnMorePrivacy')"
v-html="$t('privacyOverview') + ' ' + $t('learnMorePrivacy', learnLinks)"
>
</p>
<div
@@ -89,6 +89,10 @@ export default {
data () {
return {
hidden: false,
learnLinks: {
linkOpen: '<a href="/static/privacy" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
mounted () {
@@ -123,7 +123,6 @@
<script>
import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
@@ -236,22 +235,8 @@ export default {
},
async createOrInviteParty () {
if (this.user.party._id) {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Find Party Members',
});
this.$router.push('/looking-for-party');
} else {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Get Started',
});
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
@@ -310,6 +310,7 @@
<a
class="topbar-dropdown-item dropdown-item"
target="_blank"
rel="noopener noreferrer"
@click.prevent="openBugReportModal()"
>
{{ $t('reportBug') }}
@@ -317,6 +318,7 @@
<a
class="topbar-dropdown-item dropdown-item"
target="_blank"
rel="noopener noreferrer"
@click.prevent="openBugReportModal(true)"
>
{{ $t('askQuestion') }}
@@ -325,6 +327,7 @@
class="topbar-dropdown-item dropdown-item"
href="https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link"
target="_blank"
rel="noopener noreferrer"
>{{ $t('requestFeature') }}</a>
</div>
</li>
@@ -377,6 +380,7 @@
v-if="hasPermission(user, 'news')"
class="topbar-dropdown-item dropdown-item"
target="_blank"
rel="noopener noreferrer"
href="https://panel.habitica.com"
>
{{ $t('newsroom') }}
@@ -6,11 +6,7 @@
>
<div slot="content">
<div
v-html="$t('invitedToPartyBy', {
userId: notification.data.inviter,
userName: invitingUser.auth ? invitingUser.auth.local.username : null,
party: notification.data.name,
})"
v-html="$t('invitedToPartyBy', invitationInfo)"
>
</div>
<div class="notifications-buttons">
@@ -60,6 +56,10 @@ export default {
},
computed: {
...mapState({ user: 'user.data' }),
invitationInfo: {
usernameLink: `<a href="/profile/${this.notification.data.inviter}" target="_blank" rel="noreferrer noopener">@${this.invitingUser.auth ? this.invitingUser.auth.local.username : null}</a>`,
partyName: `<span class="notification-bold">${this.notification.data.name}</span>`,
},
},
async mounted () {
this.invitingUser = await this.$store.dispatch('members:fetchMember', {
@@ -139,8 +139,8 @@ export default {
mixins: [notifications, userStateMixin],
data () {
const abuseFlagModalBody = {
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
secondLinkStart: '<a href="/static/terms" target="_blank">',
firstLinkStart: '<a href="/static/community-guidelines" target="_blank" rel="noopener noreferrer">',
secondLinkStart: '<a href="/static/terms" target="_blank" rel="noopener noreferrer">',
linkEnd: '</a>',
};
@@ -114,7 +114,6 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal';
@@ -648,15 +647,6 @@ export default {
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
} else {
// Note a failed cron event, for our records and investigation
Analytics.track({
eventName: 'cron failed',
eventAction: 'cron failed',
eventCategory: 'behavior',
hitType: 'event',
responseCode: response.status,
}, { trackOnClient: true });
}
// Sync
@@ -197,7 +197,7 @@ export default {
},
amazonPayments: {},
assistanceEmailObject: {
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
techAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
},
sendingInProgress: false,
userReceivingGems: null,
@@ -433,9 +433,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import notificationsMixin from '@/mixins/notifications';
import paymentsMixin from '@/mixins/payments';
// analytics
import * as Analytics from '@/libs/analytics';
export default {
components: {
selectTranslatedArray,
@@ -536,16 +533,6 @@ export default {
this.close();
},
submit () {
if (this.paymentData.group && !this.paymentData.newGroup) {
Analytics.track({
hitType: 'event',
eventName: 'group plan upgrade',
eventAction: 'group plan upgrade',
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
},
@@ -67,6 +67,7 @@
<a
href="/static/privacy"
target="_blank"
rel="noopener noreferrer"
>
{{ $t('habiticaPrivacyPolicy') }}
</a>
@@ -399,7 +399,7 @@
</div>
<div
v-if="!hasGroupPlan && !canCancelSubscription"
v-html="$t(`cancelSubInfo${user.purchased.plan.paymentMethod}`)"
v-html="$t(`cancelSubInfo${user.purchased.plan.paymentMethod}`, cancelLinks)"
>
</div>
</div>
@@ -1022,6 +1022,17 @@ export default {
return nextHourglassMonth;
},
cancelLinks () {
const links = {
linkClose: '</a>',
};
if (this.user.purchased.plan.paymentMethod === this.paymentMethods.GOOGLE) {
links.linkOpen = '<a href="https://play.google.com/store/account/subscriptions" target="_blank" rel="noopener noreferrer">';
} else if (this.user.purchased.plan.paymentMethod === this.paymentMethods.APPLE) {
links.linkOpen = '<a href="https://support.apple.com/en-us/HT202039" target="_blank" rel="noopener noreferrer">';
}
return links;
},
},
mounted () {
this.$store.dispatch('common:setTitle', {
@@ -18,7 +18,7 @@
</h2>
<p
class="text-center"
v-html="$t('usernameInfo')"
v-html="$t('usernameInfo', usernameLinks)"
></p>
<username-form />
<div class="scene_veteran_pets center-block"></div>
@@ -27,7 +27,7 @@
</div>
<div
class="small text-center tos-footer"
v-html="$t('usernameTOSRequirements')"
v-html="$t('usernameTOSRequirements', requirementsLinks)"
></div>
</b-modal>
</template>
@@ -94,6 +94,15 @@ export default {
icons: Object.freeze({
helloNametag,
}),
usernameLinks: {
linkOpen: '<a href="https://habitica.fandom.com/wiki/Player_Names" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
requirementsLinks: {
termsLink: '<a href="/static/terms" target="_blank" rel="noreferrer noopener">',
guidelinesLink: '<a href="/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
};
@@ -157,7 +157,7 @@
<div
class="contact mx-auto"
>
<p v-html="$t('contactAdmin')"></p>
<p v-html="$t('contactAdmin', { adminEmail })"></p>
</div>
</div>
</div>
@@ -368,6 +368,7 @@ export default {
pixel_border: 'url(/static/npc/normal/pixel_border.png)',
},
staff: staffList,
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
};
},
};
@@ -33,8 +33,9 @@
class="drawer-help-text"
href="/static/faq#pet-foods"
target="_blank"
rel="noopener noreferrer"
>
<span>{{ $t('petLikeToEat') }}</span>
<span>{{ $t('petLikeToEat', eatLinks) }}</span>
</a>
</div>
</drawer-header-tabs>
@@ -114,6 +115,10 @@ export default {
},
],
selectedDrawerTab: this.defaultSelectedTab,
eatLinks: {
linkOpen: '<a href="/static/faq#pet-foods" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
computed: {
@@ -162,6 +162,10 @@ export default {
searchTextThrottled: null,
unfilteredCategories: [],
viewOptions: {},
customizeLinks: {
linkOpen: '<a href="">',
linkClose: '</a>',
},
};
},
computed: {
@@ -264,21 +268,21 @@ export default {
const { $t } = this;
switch (identifier) {
case 'animalEars':
return $t('allCustomizationsOwned');
return $t('allCustomizationsOwned', this.customizeLinks);
case 'animalTails':
return $t('allCustomizationsOwned');
return $t('allCustomizationsOwned', this.customizeLinks);
case 'backgrounds':
return `${$t('allCustomizationsOwned')} ${$t('checkNextMonth')}`;
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextMonth')}`;
case 'facialHair':
return $t('allCustomizationsOwned');
return $t('allCustomizationsOwned', this.customizeLinks);
case 'color':
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextSeason')}`;
case 'base':
return $t('allCustomizationsOwned');
return $t('allCustomizationsOwned', this.customizeLinks);
case 'shirt':
return $t('allCustomizationsOwned');
return $t('allCustomizationsOwned', this.customizeLinks);
case 'skin':
return `${$t('allCustomizationsOwned')} ${$t('checkNextSeason')}`;
return `${$t('allCustomizationsOwned', this.customizeLinks)} ${$t('checkNextSeason')}`;
default:
return `Unknown identifier ${identifier}`;
}
@@ -136,7 +136,7 @@
:item-margin="24"
:type="category.identifier"
:fold-button="false"
:no-items-label="$t('allEquipmentOwned')"
:no-items-label="$t('allEquipmentOwned', equipmentLinks)"
:click-handler="false"
>
<template
@@ -229,29 +229,25 @@ export default {
data () {
return {
viewOptions: {},
searchText: null,
searchTextThrottled: null,
icons: Object.freeze({
hourglass: svgHourglass,
}),
sortItemsBy: ['AZ', 'sortByNumber'],
selectedSortItemsBy: 'AZ',
selectedItemToBuy: null,
hidePinned: false,
backgroundUpdate: new Date(),
currentEvent: null,
imageURLs: {
background: '',
npc: '',
},
equipmentLinks: {
linkOpen: '<a href="/inventory/equipment">',
linkClose: '</a>',
},
};
},
computed: {
@@ -37,6 +37,9 @@ export default {
window.location.href = '/';
} else {
window.sessionStorage.setItem('apple-token', response.idToken);
if (response.email) {
window.sessionStorage.setItem('apple-email', response.email);
}
window.location.href = '/username';
}
},
@@ -107,7 +107,7 @@
<div class="body-text">
<p v-html="$t('sunsetFaqPara12')"></p>
<p v-html="$t('sunsetFaqPara13')"></p>
<p v-html="$t('sunsetFaqPara14')"></p>
<p v-html="$t('sunsetFaqPara14', translationLinks)"></p>
<p v-html="$t('sunsetFaqPara15')"></p>
<p v-html="$t('sunsetFaqPara16')"></p>
<p v-html="$t('sunsetFaqPara17')"></p>
@@ -138,9 +138,9 @@
</div>
<div class="body-text">
<ul>
<li v-html="$t('sunsetFaqList8')"></li>
<li v-html="$t('sunsetFaqList9')"></li>
<li v-html="$t('sunsetFaqList10')"></li>
<li v-html="$t('sunsetFaqList8', faqLinks)"></li>
<li v-html="$t('sunsetFaqList9', beginnerLinks)"></li>
<li v-html="$t('sunsetFaqList10', { adminEmail })"></li>
</ul>
</div>
@@ -169,6 +169,23 @@ export default {
components: {
FaqSidebar,
},
data () {
return {
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
translationLinks: {
linkOpen: '<a href="https://translate.habitica.com/projects/habitica/#information">',
linkClose: '</a>',
},
faqLinks: {
linkOpen: '<a href="https://habitica.com/static/faq">',
linkClose: '</a>',
},
beginnerLinks: {
linkOpen: '<a href="https://habitica.wordpress.com/beginning-adventurers-guide/">',
linkClose: '</a>',
},
};
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('sunsetFaqTitle'),
@@ -33,11 +33,11 @@ export default {
data () {
return {
localStorageTryFirst: {
linkStart: '<a href="/user/settings/site" target="_blank">',
linkStart: '<a href="/user/settings/site" target="_blank" rel="noopener noreferrer">',
linkEnd: '</a>',
},
localStorageTryNext: {
linkStart: '<a href="https://github.com/HabitRPG/habitica/issues/2760" target="_blank">',
linkStart: '<a href="https://github.com/HabitRPG/habitica/issues/2760" target="_blank" rel="noopener noreferrer">',
linkEnd: '</a>',
},
};
@@ -24,11 +24,11 @@
<ul>
<li><strong>{{ $t('commGuideList01A') }}</strong></li>
<li v-html="$t('commGuideList02C')"></li>
<li v-html="$t('commGuideList02N')"></li>
<li v-html="$t('commGuideList02N', { adminEmail })"></li>
<li v-html="$t('commGuideList02H')"></li>
<li v-html="$t('commGuideList02A')"></li>
<li v-html="$t('commGuideList02I')"></li>
<li v-html="$t('commGuideList02G')"></li>
<li v-html="$t('commGuideList02G', { adminEmail })"></li>
<li v-html="$t('commGuideList02D')"></li>
<li v-html="$t('commGuideList02E')"></li>
<li v-html="$t('commGuideList02O')"></li>
@@ -36,7 +36,7 @@
<li v-html="$t('commGuideList02P')"></li>
<li v-html="$t('commGuideList02Q')"></li>
<li v-html="$t('commGuideList02M')"></li>
<li v-html="$t('commGuideList02L')"></li>
<li v-html="$t('commGuideList02L', { adminEmail })"></li>
<li v-html="$t('commGuideList02J')"></li>
<li v-html="$t('commGuideList02K')"></li>
</ul>
@@ -69,7 +69,7 @@
<p v-html="$t('commGuidePara054')"></p>
<p v-html="$t('commGuidePara055')"></p>
<ul>
<li v-html="$t('commGuideList06A')"></li>
<li v-html="$t('commGuideList06A', { adminEmail })"></li>
<li v-html="$t('commGuideList06C')"></li>
<li v-html="$t('commGuideList06E')"></li>
</ul>
@@ -108,7 +108,7 @@
class="mb-3"
>
<p v-html="$t('commGuidePara061')"></p>
<p v-html="$t('commGuidePara063')"></p>
<p v-html="$t('commGuidePara063', { adminEmail })"></p>
<h2 id="meet-the-mods">
{{ $t('commGuideHeadingMeet') }}
</h2>
@@ -162,15 +162,15 @@
<h2 id="final">
{{ $t('commGuideHeadingFinal') }}
</h2>
<p v-html="$t('commGuidePara067')"></p>
<p v-html="$t('commGuidePara067', { adminEmail })"></p>
<p v-html="$t('commGuidePara068')"></p>
<h2 id="links">
{{ $t('commGuideHeadingLinks') }}
</h2>
<ul>
<li><a href="/static/faq">{{ $t('faq') }}</a></li>
<li v-html="$t('commGuideLink03')"></li>
<li v-html="$t('commGuideLink04')"></li>
<li v-html="$t('commGuideLink03', gitHubLinks)"></li>
<li v-html="$t('commGuideLink04', feedbackLinks)"></li>
</ul>
<p v-html="$t('commGuidePara069')"></p>
<ul>
@@ -187,3 +187,23 @@
</ul>
</div>
</template>
<script>
export default {
data () {
return {
adminEmail: '<a href="mailto:admin@habitica.com" target="_blank" rel="noreferrer noopener">admin@habitica.com</a>',
gitHubLinks: {
linkOpen: '<a href="https://github.com/HabitRPG/habitica" target="_blank">',
linkClose: '</a>',
},
feedbackLinks: {
linkOpen: '<a href="https://habitica.fandom.com/wiki/Habitica_Wiki" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
},
};
},
};
</script>
@@ -11,6 +11,7 @@
<br>
<a
target="_blank"
rel="noopener noreferrer"
@click.prevent="openBugReportModal()"
>
{{ $t('reportBug') }}
@@ -135,7 +135,7 @@
</h2>
<p
class="purple-600"
v-html="$t('checkGroupPlanFAQ')"
v-html="$t('checkGroupPlanFAQ', faqLinks)"
></p>
</div>
</div>
@@ -329,6 +329,10 @@ export default {
modalOption: '',
modalPage: 'account',
modalTitle: this.$t('register'),
faqLinks: {
linkOpen: '<a href="/static/faq#what-is-group-plan">',
linkClose: '</a>',
},
};
},
computed: {
@@ -47,6 +47,7 @@
class="nav-link"
href="https://habitica.wordpress.com/"
target="_blank"
rel="noopener noreferrer"
>{{ $t('companyBlog') }}</a>
</li>
<li class="nav-item">
@@ -54,6 +55,7 @@
class="nav-link"
href="https://blog.habitrpg.com/"
target="_blank"
rel="noopener noreferrer"
>{{ $t('tumblr') }}</a>
</li>
<router-link
@@ -6,7 +6,11 @@
<noscript class="banner">
{{ $t('jsDisabledHeadingFull') }}
<br />
<a href="https://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
<a
href="https://www.enable-javascript.com/"
target="_blank"
rel="noopener noreferrer"
>{{ $t('jsDisabledLink') }}</a>
</noscript>
<privacy-banner
class="privacy-banner"
@@ -254,12 +258,14 @@
class="app svg-icon"
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica"
target="_blank"
rel="noopener noreferrer"
v-html="icons.googlePlay"
></a>
<a
class="app svg-icon"
href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8"
target="_blank"
rel="noopener noreferrer"
v-html="icons.iosAppStore"
></a>
</div>
@@ -13,9 +13,10 @@
<hr>
</div>
<p>
<span v-html="$t('overviewQuestionsRevised')"></span>
<span v-html="$t('overviewQuestionsRevised', overviewLinks)"></span>
<a
target="_blank"
rel="noopener noreferrer"
@click.prevent="openBugReportModal(true)"
>
{{ $t('askQuestion') }}
@@ -57,6 +58,10 @@ export default {
shopUrl: '/shops/market',
},
},
overviewLinks: {
linkOpen: '<a href="/static/faq">',
linkClose: '</a>',
},
};
},
};
@@ -100,9 +100,11 @@
We use Google Analytics, a service which uses cookies to collect and analyze data about the use of the Services and report on activities and trends. This service may also collect data about the use of other websites, apps, and online services. You can <a
href="https://policies.google.com/technologies/partner-sites"
target="_blank"
rel="noopener noreferrer"
>learn about</a> Google's practices, and opt out of them, by downloading the <a
href="https://tools.google.com/dlpage/gaoptout"
target="_blank"
rel="noopener noreferrer"
>Google Analytics opt-out browser add-on</a>.
</p>
<h4>Controlling Cookies</h4>
@@ -687,6 +689,7 @@
<em>Opt - Out of the Sale of Personal Information or Use of Such Information for Targeted Advertising or Profiling</em>. We engage in common marketing and advertising practices to provide more relevant content and ads to users of our Site and Services. Certain of these practices may involve the selling of personal information, or the use of such information for targeted advertising or profiling, as those terms are defined in the Texas Data Privacy and Security Act (TDPSA) and the Nebraska Data Privacy Act. We do not sell personal information under the more commonly understood meaning of that wordi.e., providing personal information to third parties in exchange for money. Nor do we have actual knowledge of selling personal information of minors under the age of 16. To opt-out of the selling of your personal information, or use of that information for targeted advertising or profiling, please submit a request to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>. Note: We also treat Global Privacy Control browser signals as opt-out of sale/disclosure for targeted advertising or profiling requests. To opt-out via the Global Privacy Control, please follow the instructions available <a
href="https://globalprivacycontrol.org/"
target="_blank"
rel="noopener noreferrer"
>here</a>.
</p>
<p>
@@ -730,12 +733,15 @@
<strong>Right to lodge a complaint:</strong> Users that reside in the UK, EEA, or Switzerland have the right to seek information and assistance or lodge a complaint about our data collection and processing actions with the supervisory authority where they reside. Contact details for data protection authorities are available here. UK: <a
href="https://ico.org.uk/"
target="_blank"
rel="noopener noreferrer"
>https://ico.org.uk/</a> EEA: <a
href="https://edpb.europa.eu/about-edpb/board/members_en"
target="_blank"
rel="noopener noreferrer"
>https://edpb.europa.eu/about-edpb/board/members_en</a> Switzerland: <a
href="https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html"
target="_blank"
rel="noopener noreferrer"
>https//www.edoeb.admin.ch/edoeb/en/home/deredoeb/kontakt.html</a>.
</p>
<p>
@@ -244,13 +244,11 @@
<script>
import AppFooter from '@/components/appFooter';
import ChatBanner from '@/components/header/banners/chatBanner';
import StaticHeader from './header.vue';
export default {
components: {
AppFooter,
ChatBanner,
StaticHeader,
},
computed: {
@@ -10,6 +10,7 @@
Our Service is provided by HabitRPG, Inc. ("HabitRPG"). By accepting these Terms of Service and our Privacy Policy located at: <a
href="https://habitica.com/static/privacy"
target="_blank"
rel="noopener noreferrer"
>https://habitica.com/static/privacy</a> (collectively, the "Agreement"), using our website, Habitica.com, or our other features or services (collectively, the Services), or otherwise manifesting your assent to the Agreement, you acknowledge that you have read, understood, and agree to be legally bound by the Agreement. If you do not agree to (or cannot comply with) the Agreement, you are not permitted to access or use the Service. By accepting or agreeing to this Agreement on behalf of a company or other legal entity, you represent and warrant that you have the authority to bind that company or other legal entity to the Agreement and, in such event, "you" and "your" will refer and apply to that company or other legal entity. You further represent and warrant that your assent to this Agreement constitutes an electronic signature as defined by the Electronic Signatures in Global and National Commerce Act (E-Sign) and the Uniform Electronic Transactions Act (UETA) and that you have formed, executed, entered into, and accepted the terms of and otherwise authenticated the Agreement and acknowledged and agreed that the Agreement is an electronic record for purposes of E- Sign, UETA, and the Uniform Computer Information Transactions Act and, as such, is completely valid, has legal effect, is enforceable, and is binding on, and non- refutable by, you and/or any entity on whose behalf you are acting.
</p>
<p class="strong">
@@ -164,6 +165,7 @@
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
href="https://reportaproblem.apple.com"
target="_blank"
rel="noopener noreferrer"
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUNDS FOR APP STORE PURCHASES MADE BY CUSTOMERS.
</p>
@@ -204,6 +206,7 @@
All disputes will be resolved before a neutral arbitrator selected jointly by the parties, whose decision will be final, except for a limited right of appeal under the FAA. The arbitration shall be commenced and conducted by JAMS pursuant to its then current Comprehensive Arbitration Rules and Procedures and in accordance with the Expedited Procedures in those rules, or, where appropriate, pursuant to JAMS' Streamlined Arbitration Rules and Procedures. All applicable JAMS rules and procedures are available at the JAMS website <a
href="https://www.jamsadr.com"
target="_blank"
rel="noopener noreferrer"
>www.jamsadr.com</a>. Each party will be responsible for paying any JAMS filing, administrative, and arbitrator fees in accordance with JAMS rules. Judgment on the arbitrator's award may be entered in any court having jurisdiction. This clause shall not preclude parties from seeking provisional remedies in aid of arbitration from a court of appropriate jurisdiction. The arbitration may be conducted in person, through the submission of documents, by phone, or online. If conducted in person, the arbitration shall take place in the United States county where you reside. The parties may litigate in court to compel arbitration, to stay a proceeding pending arbitration, or to confirm, modify, vacate, or enter judgment on the award entered by the arbitrator. The parties shall cooperate in good faith in the voluntary and informal exchange of all non-privileged documents and other information (including electronically stored information) relevant to the Dispute immediately after commencement of the arbitration. As set forth below, nothing in this Agreement will prevent us from seeking injunctive relief in any court of competent jurisdiction as necessary to protect our proprietary interests.
</p>
<p>
@@ -83,7 +83,7 @@
</div>
</div>
<draggable
v-if="taskList.length > 0"
v-if="taskList.length > 0 && !rerendering"
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
@@ -432,6 +432,7 @@ export default {
selectedItemToBuy: {},
dragging: false,
rerendering: false,
};
},
computed: {
@@ -548,8 +549,8 @@ export default {
if (this.taskListOverride) originTasks = this.taskListOverride;
// Server
const taskIdToReplace = filteredList[data.newIndex];
const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
const taskIdToReplace = filteredList[data.newIndex]._id;
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace);
let newOrder;
if (taskToMove.group.id && !this.isUser) {
@@ -568,6 +569,9 @@ export default {
// Client
const deleted = originTasks.splice(data.oldIndex, 1);
originTasks.splice(data.newIndex, 0, deleted[0]);
this.rerendering = true;
await this.$nextTick();
this.rerendering = false;
},
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
+5 -17
View File
@@ -13,6 +13,8 @@
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
tabindex="0"
@keypress.enter="$emit('editTask', task)"
>
<div
class="d-flex"
@@ -98,9 +100,7 @@
<div
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
>
<div class="d-flex justify-content-between">
<h3
@@ -432,10 +432,6 @@
outline: none;
transition: none;
border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 4px;
}
}
.control-bottom-box {
@@ -462,16 +458,13 @@
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
}
.task:not(.groupTask) {
&:hover,
&:focus-within {
.left-control, .right-control, .task-content {
border-color: $purple-400;
}
&:hover, &:focus {
border: none;
outline: 1px solid $purple-400;
}
}
@@ -522,11 +515,6 @@
&-user {
padding-right: 0px;
}
&:focus {
border-radius: 4px;
border: $purple-400 solid 1px;
}
}
.task-title + .task-dropdown ::v-deep .dropdown-menu {
@@ -55,11 +55,31 @@
</div>
</div>
<div class="form-group">
<lockable-label
:class-override="cssClass('headings')"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<div class="d-flex align-items-center">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<div
id="spi-alert"
class="d-flex align-items-center"
:class="cssClass('headings')"
>
<div
class="svg svg-icon color icon-16 mr-1"
v-html="icons.alert"
></div>
<small
class="my-1"
>
<a
:class="cssClass('headings')"
>{{ $t('avoidSPI') }}</a>
</small>
</div>
</div>
<input
ref="inputToFocus"
v-model="task.text"
@@ -79,10 +99,20 @@
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
<b-popover
:target="'spi-alert'"
triggers="hover"
placement="bottom"
offset="-128"
>
<div
v-html="$t('avoidSPIDetails', spiLinkData)">
</div>
</b-popover>
<div
class="form-group mb-0"
>
<div class="d-flex">
<div class="d-flex align-items-center">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
@@ -382,6 +412,25 @@
</div>
</div>
</div>
<p
v-if="task.type === 'daily' && schedulingSummary"
class="scheduling-summary mt-2 mb-0"
>
{{ schedulingSummary }}
</p>
<div
v-if="task.type === 'daily' && schedulingWarning"
class="scheduling-warning mt-2"
>
<span
class="scheduling-warning-icon svg-icon color gray-50"
v-html="icons.alert"
></span>
<span
class="scheduling-warning-text"
v-html="schedulingWarning"
></span>
</div>
<div
v-if="!groupId"
class="tags-select option mt-3"
@@ -963,6 +1012,20 @@
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
}
}
.b-popover {
margin-top: -5px;
max-width: 330px;
}
.popover-body {
text-align: left;
a {
color: $gray-500;
text-decoration: underline;
}
}
}
@media only screen and (max-width: 768px) {
@@ -1065,6 +1128,42 @@
height: 1rem;
}
.scheduling-summary {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 12px;
line-height: 16px;
color: $gray-50;
text-align: left;
}
.scheduling-warning {
display: flex;
align-items: flex-start;
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-style: normal;
font-size: 12px;
line-height: 16px;
color: $gray-50;
}
.scheduling-warning-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
flex-shrink: 0;
margin-right: 6px;
margin-top: -1px;
}
.scheduling-warning-text {
flex: 1;
}
label {
display: inline-flex;
align-items: center;
@@ -1196,6 +1295,7 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
import calendarIcon from '@/assets/svg/calendar.svg?raw';
import gripIcon from '@/assets/svg/grip.svg?raw';
import InformationIcon from '@/components/ui/informationIcon.vue';
import alertIcon from '@/assets/svg/for-css/alert-white.svg?raw';
export default {
components: {
@@ -1231,6 +1331,7 @@ export default {
streak: streakIcon,
calendar: calendarIcon,
grip: gripIcon,
alert: alertIcon,
}),
members: [],
membersNameAndId: [],
@@ -1251,6 +1352,11 @@ export default {
{ key: 'per', label: 'perception', description: 'perTaskText' },
],
calendarHighlights: { dates: [new Date()] },
spiLinkData: {
firstLink: '<a href="/static/privacy#section_1" target="_blank" rel="noopener noreferrer">',
secondLink: '<a href="/static/privacy" target="_blank" rel="noopener noreferrer">',
linkClose: '</a>',
},
};
},
computed: {
@@ -1326,6 +1432,87 @@ export default {
}
return null;
},
schedulingSummary () {
if (!this.task || this.task.type !== 'daily') return '';
const { task } = this;
const everyXValue = +task.everyX;
let interval;
if (task.frequency === 'daily') {
interval = everyXValue === 1 ? this.$t('everyDay') : this.$t('everyXDays', { count: everyXValue });
} else if (task.frequency === 'weekly') {
interval = everyXValue === 1 ? this.$t('everyWeek') : this.$t('everyXWeeks', { count: everyXValue });
} else if (task.frequency === 'monthly') {
interval = everyXValue === 1 ? this.$t('everyMonth') : this.$t('everyXMonths', { count: everyXValue });
} else if (task.frequency === 'yearly') {
interval = everyXValue === 1 ? this.$t('everyYear') : this.$t('everyXYears', { count: everyXValue });
} else {
return '';
}
let details = '';
if (task.frequency === 'weekly') {
const dayNames = {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
};
const activeDays = Object.keys(task.repeat || {}).filter(d => task.repeat[d]);
if (activeDays.length > 0) {
details = ` on ${activeDays.map(d => dayNames[d]).join(', ')}`;
}
} else if (task.frequency === 'monthly' && task.startDate) {
const dayOfMonth = moment(task.startDate).date();
if (task.weeksOfMonth && task.weeksOfMonth.length > 0) {
const weekNum = task.weeksOfMonth[0] + 1;
const weekStr = String(weekNum);
const lastDigit = weekStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && weekStr !== '11') suffix = 'st';
if (lastDigit === '2' && weekStr !== '12') suffix = 'nd';
if (lastDigit === '3' && weekStr !== '13') suffix = 'rd';
const dayName = moment(task.startDate).format('dddd');
details = ` on the ${weekNum}${suffix} ${dayName} of the month`;
} else if (task.daysOfMonth && task.daysOfMonth.length > 0) {
const dom = task.daysOfMonth[0];
const domStr = String(dom);
const lastDigit = domStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
details = ` on the ${dom}${suffix}`;
} else {
const domStr = String(dayOfMonth);
const lastDigit = domStr.slice(-1);
let suffix = 'th';
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
details = ` on the ${dayOfMonth}${suffix}`;
}
} else if (task.frequency === 'yearly' && task.startDate) {
details = ` on ${moment(task.startDate).format('MMMM Do')}`;
}
return `${this.$t('repeats')} ${interval}${details}`;
},
schedulingWarning () {
if (!this.task || this.task.type !== 'daily') return '';
const { task } = this;
if (task.frequency === 'monthly'
&& task.weeksOfMonth && task.weeksOfMonth.length > 0
&& task.weeksOfMonth[0] === 4
&& task.startDate) {
const dayName = moment(task.startDate).format('dddd');
return this.$t('fifthWeekWarning', { day: dayName });
}
return '';
},
repeatsOn: {
get () {
let repeatsOn = 'dayOfMonth';
@@ -1399,7 +1586,7 @@ export default {
this.task.down = !this.task.down;
},
weekdaysMin (dayNumber) {
return moment.weekdaysMin(dayNumber);
return this.$t(`weekdaysMin${dayNumber}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
@@ -222,14 +222,22 @@ export default {
return usernames;
},
summarySentence () {
let fifthWeekWarning = '';
if (this.task.type === 'daily' && this.task.frequency === 'monthly'
&& this.task.weeksOfMonth && this.task.weeksOfMonth.length > 0
&& this.task.weeksOfMonth[0] === 4) {
const activeDays = keys(pickBy(this.task.repeat, value => value === true));
const dayName = this.expandDayString[activeDays[0]];
fifthWeekWarning = ` ${this.$t('fifthWeekWarning', { day: dayName })}`;
}
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.${fifthWeekWarning}`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.${fifthWeekWarning}`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
@@ -287,25 +295,14 @@ export default {
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
const weekNum = weeksOfMonth[0] + 1;
const weekNumStr = String(weekNum);
const lastDigit = weekNumStr.slice(-1);
let ordinalSuffix = 'th';
if (lastDigit === '1' && weekNumStr !== '11') ordinalSuffix = 'st';
if (lastDigit === '2' && weekNumStr !== '12') ordinalSuffix = 'nd';
if (lastDigit === '3' && weekNumStr !== '13') ordinalSuffix = 'rd';
dayStringArray.push(`${weekNum}${ordinalSuffix}`);
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
@@ -343,9 +340,8 @@ export default {
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
if (numericX === 1) return `<strong>${this.$t('everyMonth')}</strong>`;
return `<strong>${this.$t('everyXMonths', { count: numericX })}</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
@@ -68,8 +68,12 @@ export default {
},
methods: {
upDate (after) {
this.value = after;
this.$emit('update:date', after);
// zero out the time so the server doesn't shift the day across a DST boundary on save
const normalized = after
? new Date(after.getFullYear(), after.getMonth(), after.getDate())
: null;
this.value = normalized;
this.$emit('update:date', normalized);
},
setToday () {
this.upDate(moment().toDate());
@@ -377,7 +377,7 @@
<div class="">
<div
class="alert alert-info alert-sm"
v-html="$t('communityGuidelinesWarning', managerEmail)"
v-html="$t('communityGuidelinesWarning', communityGuidelineLinks)"
></div>
<!-- TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak-->
<div class="form-group">
@@ -1060,8 +1060,10 @@ export default {
blurb: '',
},
hero: {},
managerEmail: {
hrefBlankCommunityManagerEmail: `<a href="mailto:${COMMUNITY_MANAGER_EMAIL}">${COMMUNITY_MANAGER_EMAIL}</a>`,
communityGuidelineLinks: {
linkOpen: '<a href="https://habitica.com/static/community-guidelines" target="_blank" rel="noreferrer noopener">',
linkClose: '</a>',
adminEmail: `<a href="mailto:${COMMUNITY_MANAGER_EMAIL}" target="_blank" rel="noopener noreferrer">${COMMUNITY_MANAGER_EMAIL}</a>`,
},
selectedPage: 'profile',
achievements: {},
@@ -89,7 +89,7 @@
v-if="userLevel100Plus"
v-once
class="level-100-message"
v-html="$t('noMoreAllocate')"
v-html="$t('noMoreAllocate', allocateLinks)"
></div>
</div>
<div class="row allocation-boxes-row">
@@ -395,14 +395,12 @@ export default {
_skip: 'skip',
shield: this.$t('offHandCapitalized'),
},
allocateStatsList: {
str: { title: 'allocateStr', popover: 'strengthText', allocatepop: 'allocateStrPop' },
int: { title: 'allocateInt', popover: 'intText', allocatepop: 'allocateIntPop' },
con: { title: 'allocateCon', popover: 'conText', allocatepop: 'allocateConPop' },
per: { title: 'allocatePer', popover: 'perText', allocatepop: 'allocatePerPop' },
},
stats: {
str: {
title: 'strength',
@@ -422,6 +420,10 @@ export default {
},
},
content: Content,
allocateLinks: {
linkOpen: '<a href="/shops/market">',
linkClose: '</a>',
},
};
},
computed: {
@@ -248,7 +248,7 @@
v-if="userLevel100Plus"
v-once
>
{{ $t('noMoreAllocate') }}
{{ $t('noMoreAllocate', allocateLinks) }}
</p>
<p
v-if="user.stats.points || userLevel100Plus"
@@ -480,6 +480,10 @@ export default {
con: { title: 'allocateCon', popover: 'conText', allocatepop: 'allocateConPop' },
per: { title: 'allocatePer', popover: 'perText', allocatepop: 'allocatePerPop' },
},
allocateLinks: {
linkOpen: '<a href="/shops/market">',
linkClose: '</a>',
},
};
},
computed: {

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