Compare commits

...

57 Commits

Author SHA1 Message Date
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
Kalista Payne cc7683a871 chore(git): update submodule 2026-03-19 18:09:22 -05:00
Kalista Payne 31b2781333 Squashed commit of the following:
commit 866f074a15
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Mar 18 15:51:18 2026 -0500

    fix(quests): remove backticks from text

commit d06fc2825e
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Mar 18 13:16:32 2026 -0500

    fix(background): add missing data

commit 55156c5f80
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Mar 18 13:00:43 2026 -0500

    fix(lint): max-len

commit db88092acf
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Mar 18 12:56:30 2026 -0500

    fix(customization): show event backgrounds for AF

commit d6fd1ce7fa
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Mar 17 15:55:53 2026 +0100

    set release date

commit b876d8c789
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Mar 17 11:40:17 2026 +0100

    Improve swap handling

commit f75a4d0147
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Mar 17 11:22:42 2026 +0100

    use correct name for bear sprites

commit 195db1b132
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Mar 16 23:01:35 2026 +0100

    more fix

commit a42d3f08d7
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Mar 16 18:43:21 2026 +0100

    fix test

commit 9c06299c34
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Mar 16 09:13:13 2026 +0100

    AF tweaks

commit 5465e23e67
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Mar 11 19:43:01 2026 -0500

    fix(sprites): add missing gif redirects

commit 5721ecc6f2
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Mar 10 16:34:26 2026 -0500

    chore(css): run sprites

commit 2184ff2e69
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Mar 10 10:38:34 2026 -0500

    fix(test): date

commit d684a7297c
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Mar 10 10:32:04 2026 -0500

    fix(test): update for 2026, also more lint

commit 82e7947fd7
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Mar 10 10:22:05 2026 -0500

    fix(event): lint and missing pieces

commit 96818b2d77
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Mar 9 20:11:22 2026 -0500

    feat(event): finished Alien build

commit 4c9763b676
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Mar 6 16:30:36 2026 -0600

    wip(event): April Fools 2026 build

commit 2f16a016e6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Feb 4 14:16:50 2026 +0100

    add april fools tests

commit cd1d926c98
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Feb 4 14:09:16 2026 +0100

    make april fools cycle through

commit d8a4216c41
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Feb 2 14:41:26 2026 +0100

    fix lint

commit 8265a15923
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Feb 2 12:34:46 2026 +0100

    name key more generic

commit 9c7bde8ad5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Feb 2 12:26:43 2026 +0100

    right date for april fools

commit c2b92c6311
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Feb 2 12:26:19 2026 +0100

    rework how april fools works
2026-03-19 15:09:32 -05:00
Kalista Payne d37d3bc5ac 5.46.4 2026-03-17 14:51:06 -05:00
Weblate ef3a28791e Translated using Weblate (Dutch)
Currently translated at 72.8% (2586 of 3551 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 95.0% (19 of 20 strings)

Translated using Weblate (Dutch)

Currently translated at 63.6% (161 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 70.0% (14 of 20 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (927 of 941 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (20 of 20 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Korean)

Currently translated at 43.3% (82 of 189 strings)

Translated using Weblate (Korean)

Currently translated at 33.8% (64 of 189 strings)

Co-authored-by: Alexander Tschigir <tchi.gugl@gmail.com>
Co-authored-by: Jildau Bras <jildaubras@gmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tijl Casteleyn <casteleyntijl@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 성명 <phjk2536@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Rebirth
Translation: Habitica/Settings
2026-03-17 10:33:27 +01:00
Kalista Payne c3c2607bca Remove join retired public guild loophole (#15626)
* fix(guilds): remove join retired public guild loophole

* fix(groups): adjust test expectations
and move some business logic back out of the controller

* test(challenges): add cases related to public Guilds, Tavern and otherwise

* fix(tests): more erroneously public test guilds, lint

* fix(tests): still more setup tweaks

* fix(lint): whitespace

* fix(tests): couple more adjustments

* fix(test): last challenge issue??
2026-03-12 19:15:00 -05:00
Tanmay Nalawade 7a6d64f158 Duplicate tags Bug solved (#15615)
* added hasExactMatch function

* changed the handleSubmit logic to check for match

* changed the conditional in class for dropdown

* added hasExactMatch conditional to display the AddTag text

* one more conditional to check if string is empty

* changed styling to display hidden text of "Press Enter to add tag"

* lint error fixed

* fix(tags): don't allow same tag spam within add session

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2026-03-12 19:09:30 -05:00
Kalista Payne 836e63246d fix(emails): correct variable name in group plan invites 2026-03-12 18:58:49 -05:00
Kalista Payne 61585b2549 5.46.3 2026-03-12 16:51:19 -05:00
Corban Villa 07275bd522 fix(security): don't reuse IV for cryptography 2026-03-12 14:53:30 -05:00
Fiz 74fc543ef2 Orb of Rebirth Updates (Count usage) (#15629)
* Update orb of rebirth to count every usage

* Orb of rebirth modal UI updates

* Fix rebirth modal showing again after page refresh

* remove unused MAX_LEVEL

* scale orb of rebirth on modal

* UI & wording updates for Orb of Rebirth

* UI & wording updates for Orb of Rebirth cont.

* Orb of rebirth UI tweaks

* Orb of rebirth UI tweaks

* Orb of Rebirth modal UI tweak

* Extend modal waves

* Continued Orb of Rebirth UI Updates

* Orb of rebirth margin tweak

* rebirth-orb asset
2026-03-12 14:51:48 -05:00
Kalista Payne 8c90e5472b 5.46.2 2026-03-10 12:02:09 -05:00
Kalista Payne c8d9ba6c8e chore(git): update submodule 2026-03-10 12:02:06 -05:00
Weblate 1675c2749b Merge branch 'origin/develop' into Weblate. 2026-03-10 17:58:10 +01:00
Rohithgowda K e85a2bae14 fix: prevent duplicate streak achievement notifications (#13325) (#15550)
* fix: prevent duplicate streak achievement notifications (#13325)

* fix(lint): whitespace
also remove unnecessary file and comments

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-03-10 10:03:28 -05:00
Weblate 3355500fba Translated using Weblate (French)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (277 of 279 strings)

Translated using Weblate (Czech)

Currently translated at 78.5% (739 of 941 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (278 of 279 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Portuguese)

Currently translated at 68.7% (189 of 275 strings)

Translated using Weblate (Portuguese)

Currently translated at 86.6% (383 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2115 of 3551 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.1% (269 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 91.4% (267 of 292 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.0% (438 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.4% (2112 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Korean)

Currently translated at 99.2% (934 of 941 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.0% (184 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 46.2% (117 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 99.0% (932 of 941 strings)

Co-authored-by: Anderson Ferreira <freiitas.dev@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Duggu Ghosh <duggu52d@gmail.com>
Co-authored-by: Isabela de França <ifranceg@gmail.com>
Co-authored-by: Kim Sihyung <kimsihyung@u.nus.edu>
Co-authored-by: Matej Boura <B.Matej@email.cz>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Translatlantic Translation <translatlanticom@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2026-03-10 12:48:18 +01:00
Kalista Payne 486f15df0f fix(news): allow X dismiss for everyone 2026-03-09 15:00:47 -05:00
Kalista Payne f0b6b5611c feat(news): allow quick dismiss for admins 2026-03-06 19:07:48 -06:00
Kalista Payne 7e45c79714 chore(npm): update lockfiles 2026-03-06 11:58:02 -06:00
Kalista Payne 8da6065355 fix(pins): don't erase pinned quest potions 2026-03-06 11:22:29 -06:00
Kalista Payne a212363bda 5.46.1 2026-03-05 14:59:58 -06:00
Weblate 2e19e73b9e Translated using Weblate (Dutch)
Currently translated at 72.7% (2585 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3466 of 3551 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Czech)

Currently translated at 13.0% (33 of 253 strings)

Translated using Weblate (Czech)

Currently translated at 98.2% (164 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (3497 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 50.6% (148 of 292 strings)

Translated using Weblate (German)

Currently translated at 98.1% (434 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (940 of 941 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (441 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Russian)

Currently translated at 74.7% (189 of 253 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (German)

Currently translated at 92.8% (271 of 292 strings)

Translated using Weblate (French)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (German)

Currently translated at 99.5% (937 of 941 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (French)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (French)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 80.4% (152 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 54.5% (138 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 39.1% (99 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 35.5% (90 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (653 of 862 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (653 of 862 strings)

Translated using Weblate (Turkish)

Currently translated at 33.9% (86 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 95.3% (410 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 94.8% (408 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Turkish)

Currently translated at 94.6% (407 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 73.4% (2567 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 82.3% (354 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 67.1% (2349 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.4% (1731 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.4% (1731 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 86.2% (238 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 67.1% (2349 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 88.8% (382 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 88.6% (381 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 88.3% (380 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 73.1% (2557 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Dutch)

Currently translated at 58.8% (149 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Dutch)

Currently translated at 96.9% (127 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 76.0% (210 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 56.5% (143 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 85.5% (368 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.3% (150 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 95.9% (894 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (201 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.5% (193 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.6% (109 of 114 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 49.0% (1717 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 76.4% (2672 of 3497 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 61.4% (169 of 275 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Swedish)

Currently translated at 59.6% (164 of 275 strings)

Translated using Weblate (Swedish)

Currently translated at 87.7% (115 of 131 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Swedish)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (3434 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (251 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 92.4% (134 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 52.1% (144 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 63.5% (183 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 48.2% (139 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 47.2% (1653 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2447 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Swedish)

Currently translated at 4.7% (12 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 54.1% (137 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 93.5% (872 of 932 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 92.5% (187 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 52.5% (133 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 51.7% (131 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Dutch)

Currently translated at 91.8% (856 of 932 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Dutch)

Currently translated at 91.6% (854 of 932 strings)

Translated using Weblate (Portuguese)

Currently translated at 52.7% (1844 of 3497 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 51.0% (141 of 276 strings)

Translated using Weblate (Swedish)

Currently translated at 77.6% (73 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 89.9% (170 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 4.7% (12 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Swedish)

Currently translated at 58.5% (546 of 932 strings)

Translated using Weblate (Swedish)

Currently translated at 86.2% (163 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 85.7% (162 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Swedish)

Currently translated at 70.3% (133 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 86.1% (248 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Swedish)

Currently translated at 59.2% (163 of 275 strings)

Translated using Weblate (Czech)

Currently translated at 9.0% (23 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 73.0% (2556 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Czech)

Currently translated at 8.6% (22 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (200 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 79.8% (230 of 288 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 90.4% (843 of 932 strings)

Translated using Weblate (German)

Currently translated at 99.6% (3485 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.5% (3480 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3476 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (198 of 202 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Japanese)

Currently translated at 98.2% (112 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 85.9% (801 of 932 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3475 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.6% (252 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 98.8% (250 of 253 strings)

Translated using Weblate (German)

Currently translated at 98.8% (250 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 84.0% (783 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Dutch)

Currently translated at 49.4% (125 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 96.7% (88 of 91 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3473 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3473 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (3451 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.3% (3440 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 72.5% (2536 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.6% (3380 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 72.2% (2528 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 99.1% (243 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 99.4% (188 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (German)

Currently translated at 99.2% (3470 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.2% (112 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (German)

Currently translated at 96.5% (140 of 145 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.1% (3468 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.1% (3466 of 3497 strings)

Translated using Weblate (Swedish)

Currently translated at 94.6% (53 of 56 strings)

Translated using Weblate (German)

Currently translated at 99.0% (3464 of 3497 strings)

Translated using Weblate (Portuguese)

Currently translated at 93.3% (14 of 15 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 68.5% (591 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 91.0% (184 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.9% (1885 of 3497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (928 of 932 strings)

Co-authored-by: Adam Rimanek <adamrimanek.imsp@gmail.com>
Co-authored-by: Alison Alex <spamkari@hotmail.com>
Co-authored-by: Andrés Leiva Barco <andresleibar@gmail.com>
Co-authored-by: Artemis <circlegohard@gmail.com>
Co-authored-by: BMA <simpintis@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Daisy Hsu <HsuD@stmaryscambridge.co.uk>
Co-authored-by: Deleted User <noreply+908@weblate.org>
Co-authored-by: Duggu Ghosh <duggu52d@gmail.com>
Co-authored-by: DumbDump <Schernova13@yandex.ru>
Co-authored-by: Fake Name <alpasalp220@gmail.com>
Co-authored-by: Gizem <gizem100296@gmail.com>
Co-authored-by: Harry Erickson <harry3rickson@gmail.com>
Co-authored-by: Jamie Herbert <pth23tpg@bangor.ac.uk>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Jeremia Hölscher <holscherjury@gmail.com>
Co-authored-by: Jildau Bras <jildaubras@gmail.com>
Co-authored-by: Julius Eikmans <jcs.e@icloud.com>
Co-authored-by: Lawan Fathullah <fathullahlawan@gmail.com>
Co-authored-by: Lizard <li07369427zard@gmail.com>
Co-authored-by: Lucas Vitor de Farias <Lucasvi90robloxbr@gmail.com>
Co-authored-by: Luidson Alejandro Froz Paiva <Luidpah1@gmail.com>
Co-authored-by: Léa <ambredf@outlook.fr>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Mia S <miveliina@gmail.com>
Co-authored-by: Nora <yenyisun@gmail.com>
Co-authored-by: Prayaag Thakkar <prayaag13@outlook.com>
Co-authored-by: Rafael Hoyos Arango <rafa131842@gmail.com>
Co-authored-by: SY <smartyeti@proton.me>
Co-authored-by: Sebastian Ramorino <sebarc2005@gmail.com>
Co-authored-by: Sergey <sergejepihin323@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: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Translatlantic Translation <translatlanticom@gmail.com>
Co-authored-by: Uwe B <hbtca@tunixgut.de>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaezch <dkrovel@gmail.com>
Co-authored-by: Yorick Steffens <yorick.steffens@gmail.com>
Co-authored-by: Zarah Lundberg <sar_lun@hotmail.com>
Co-authored-by: donkie <lllllllovinllllll@gmail.com>
Co-authored-by: kim ham <kim@pfts.se>
Co-authored-by: viyu viyu <gnaremoob@binotuz.com>
Co-authored-by: いんこ <ayakabooker@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/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
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/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/death/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
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/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/front/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/tr/
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/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
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/generic/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
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/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
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/subscriber/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/de/
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/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
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-03-05 11:27:29 +01:00
Hafiz 1047b0e03b up habitica-markdown -> 4.1.0 & defensive check 2026-03-02 14:32:58 -06:00
Kalista Payne 159f850bd1 fix(avatar): correct margin override in buy modal 2026-02-27 15:09:11 -06:00
Fiz 42083efb7e Emojis Update (#15620)
* Add group plan selection modal for upgrades

Allow users to select an existing group to upgrade before creating a new one.

* crlf -> lf lint

* set selection of group plan

Also tiny UI fixes

* Update group plan selection to include expired plans

* Add includeExpiredPlans option to group fetching

* force flag when fetching group plans

* Update group plan eligibility check

* Fix eslint error in push notification import

* replace chaining (?.) w/null check

* Remove comment

* set initial selected group plan, and fix card rounding

* format member count

* Show warning for pending party invites when upgrading to paid group plan

Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites

* suppress error toasts for group modal, and UI tweaks for group modal

suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans

* Clear upgradingGroup state after group plan payment

* Update emoji system to native Unicode rendering

* Fix line endings in habiticaMarkdown test

* fix indented code block detection for markdown-it v14

* update habitica-markdown to include v3 emoji dataset  (pointed towards test branch)

* size emoji in markdown

* emoji autocomplete to chat, messages, tasks, and profile

add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper)

* try upping github-action fix

* trying another github actions fix

* update habitica-markdown package version (v3.0.0 -> v4.0.0)

* Fix emoji autocomplete overlapping actual text

position dropdown below text

* update group-plans info card styles

* Support Melior emoji autocomplete & more places for emoji autocomplete

Include emoji autocomplete in task checklists, tags, challenge name/summary/description

* position emoji autocomplete dropdown below text area

* fix: replace nested ternary

* Emoji autocomplete fixes

Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete

* Have emoji autocomplete dropdown directly below text, add to task tag

* Fix emoji autocomplete starting at beginning/end initially

* lint/line length

* Add group plan selection modal for upgrades

Allow users to select an existing group to upgrade before creating a new one.

* crlf -> lf lint

* set selection of group plan

Also tiny UI fixes

* Update group plan selection to include expired plans

* Add includeExpiredPlans option to group fetching

* force flag when fetching group plans

* Update group plan eligibility check

* Fix eslint error in push notification import

* replace chaining (?.) w/null check

* Remove comment

* set initial selected group plan, and fix card rounding

* format member count

* Show warning for pending party invites when upgrading to paid group plan

Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites

* suppress error toasts for group modal, and UI tweaks for group modal

suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans

* Clear upgradingGroup state after group plan payment

* Update emoji system to native Unicode rendering

* Fix line endings in habiticaMarkdown test

* fix indented code block detection for markdown-it v14

* update habitica-markdown to include v3 emoji dataset  (pointed towards test branch)

* size emoji in markdown

* emoji autocomplete to chat, messages, tasks, and profile

add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper)

* try upping github-action fix

* trying another github actions fix

* update habitica-markdown package version (v3.0.0 -> v4.0.0)

* Fix emoji autocomplete overlapping actual text

position dropdown below text

* update group-plans info card styles

* Support Melior emoji autocomplete & more places for emoji autocomplete

Include emoji autocomplete in task checklists, tags, challenge name/summary/description

* position emoji autocomplete dropdown below text area

* fix: replace nested ternary

* Emoji autocomplete fixes

Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete

* Have emoji autocomplete dropdown directly below text, add to task tag

* Fix emoji autocomplete starting at beginning/end initially

* lint/line length

* Revert "trying another github actions fix"

This reverts commit 72fc7fc20e.

* Revert "try upping github-action fix"

This reverts commit 70e48a57aa.

* fix(git): revert ci changes

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-02-26 16:36:37 -06:00
Fiz f21e800b0b Group Plan Modal (#15588)
* Add group plan selection modal for upgrades

Allow users to select an existing group to upgrade before creating a new one.

* crlf -> lf lint

* set selection of group plan

Also tiny UI fixes

* Update group plan selection to include expired plans

* Add includeExpiredPlans option to group fetching

* force flag when fetching group plans

* Update group plan eligibility check

* Fix eslint error in push notification import

* replace chaining (?.) w/null check

* Remove comment

* set initial selected group plan, and fix card rounding

* format member count

* Show warning for pending party invites when upgrading to paid group plan

Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites

* suppress error toasts for group modal, and UI tweaks for group modal

suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans

* Clear upgradingGroup state after group plan payment
2026-02-26 16:02:47 -06:00
Kalista Payne 40122e5621 5.46.0 2026-02-26 12:08:13 -06:00
Kalista Payne 0ae19d9107 Squashed commit of the following:
commit 963b4133ec
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Feb 19 15:48:41 2026 -0600

    fix(text): clean up some gear descriptions

commit 53999a5b80
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:18:07 2026 -0600

    fix(content): add seasonal set tokens

commit 4510c90e41
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:10:24 2026 -0600

    feat(content): March-May 2026
2026-02-24 12:02:52 -06:00
Kalista Payne 68bfebcf30 5.45.0 2026-02-24 10:18:25 -06:00
Phillip Thelen 3e93911e70 Phillip/prod perf2 (#15596)
* Add new api call for kubernetes startup probe

* add hostname as tag for loggly

* Only listen to one change

* increase vite min chunk size

* respond gracefully to shutdown signal

* update server readiness according to mongodb and redis connection

* make larger vite chunks

* fix lint
2026-02-24 10:17:21 -06:00
Kalista Payne 4ea8636f03 fix(profile): correct stat display for class gear 2026-02-20 13:19:56 -06:00
Kalista Payne 9f97a09b8c fix(export): remove deprecated routes 2026-02-19 15:45:38 -06:00
Phillip Thelen eccc115b73 Admin Panel fixes (#15613)
* fix profile link to admin panel

* fix profile looking broken when no background is equipped
2026-02-19 11:11:25 -06:00
Tanmay Nalawade 2b26eb2bd1 Habit and Daily task counts not displaying for certain filters (#15595)
* habits-all fixed

* dailies section fixed

* to do counter fixed

* Remove linting changes and apply logic fix with single quotes

* refactor(tasks): simpler badgeCount logic

* fix(lint): remove unused import

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-02-19 11:09:40 -06:00
Kalista Payne 8e042cabc4 5.44.3 2026-02-10 17:37:48 -06:00
Kalista Payne 8abe167848 chore(subproj): update habitica-images 2026-02-10 17:37:45 -06:00
Kalista Payne 3414f962e2 fix(lint): string style and whitespace 2026-02-10 17:13:02 -06:00
Kalista Payne 1b68e6d4d3 fix(event): show Spring Seasonal Shop on Valentines 2026-02-10 17:07:47 -06:00
Kalista Payne 5dd9711413 Update local dev MongoDB versions (#15334)
* chore(mongodb): update local dev MongoDB versions

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

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

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

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* fix(mongo): start replica set

* run mongo as gh action service

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

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

* mongo docker for testing; align mongodb directory naming

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

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

* fix oudated healthcheck param

* chore(mongodb): update local dev MongoDB versions

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

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

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

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* run mongo as gh action service

* fix(mongo): start replica set

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

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

* mongo docker for testing; align mongodb directory naming

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

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

* fix oudated healthcheck param

* fix(config): remove dup keys

* using npx vite during docker aio run

---------

Co-authored-by: negue <eugen.bolz@gmail.com>
2026-01-30 17:19:35 -06:00
343 changed files with 12584 additions and 5807 deletions
+18 -8
View File
@@ -82,7 +82,7 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:sanity
common:
runs-on: ubuntu-latest
strategy:
@@ -129,13 +129,13 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:content
api-unit:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -144,11 +144,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -158,15 +160,17 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api:unit
env:
REQUIRES_SERVER=true: true
api-v3-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -176,10 +180,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -189,15 +194,18 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v3:integration
env:
REQUIRES_SERVER=true: true
api-v4-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -207,10 +215,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -220,6 +229,7 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v4:integration
env:
REQUIRES_SERVER=true: true
+1 -1
View File
@@ -47,5 +47,5 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data*
/mongodb-*
/.nyc_output
+2 -3
View File
@@ -46,7 +46,7 @@
"MAINTENANCE_MODE": "false",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs&directConnection=true&readPreference=secondary",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
@@ -75,7 +75,6 @@
"S3_ACCESS_KEY_ID": "accessKeyId",
"S3_BUCKET": "bucket",
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SESSION_SECRET": "YOUR SECRET HERE",
"SITE_HTTP_AUTH_ENABLED": "false",
@@ -90,7 +89,7 @@
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs&directConnection=true&readPreference=secondary",
"TIME_TRAVEL_ENABLED": "false",
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"WEB_CONCURRENCY": 1
-53
View File
@@ -1,53 +0,0 @@
services:
client:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev"]
depends_on:
- server
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "8080:8080"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: mongo:5.0.23
restart: unless-stopped
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
networks:
- habitica
ports:
- "27017:27017"
networks:
habitica:
driver: bridge
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-only"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-test"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker-testing:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+43 -22
View File
@@ -1,35 +1,56 @@
version: "3"
services:
client:
build: .
networks:
- habitica
environment:
- BASE_URL=http://server:3000
ports:
- "8080:8080"
command: ["npm", "run", "client:dev"]
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev:docker"]
depends_on:
- server
server:
build: .
ports:
- "3000:3000"
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "5173:5173"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
depends_on:
- mongo
mongo:
image: mongo:3.6
ports:
- "27017:27017"
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: "mongo:7.0"
container_name: "habitica-mongodb"
networks:
- habitica
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
networks:
habitica:
+12 -9
View File
@@ -5,7 +5,7 @@ import path from 'path';
import babel from 'gulp-babel';
import os from 'os';
import fs from 'fs';
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
import spawn from 'cross-spawn';
import clean from 'rimraf';
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
@@ -35,7 +35,7 @@ gulp.task('build:prod', gulp.series(
// When used on windows `run-rs` must first be run without the `--keep` option
// in order to be setup correctly, afterwards it can be used.
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
const MONGO_PATH = path.join(__dirname, '/../mongodb-data-docker/');
gulp.task('build:prepare-mongo', async () => {
if (fs.existsSync(MONGO_PATH)) {
@@ -51,29 +51,32 @@ gulp.task('build:prepare-mongo', async () => {
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
// use run-rs without --keep, kill it as soon as the replica set starts
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
const dockerMongoProcess = spawn('npm', ['run', 'docker:mongo:dev']);
for await (const chunk of runRsProcess.stdout) {
let manuallyStopped = false;
for await (const chunk of dockerMongoProcess.stdout) {
const stringChunk = chunk.toString();
console.log(stringChunk); // eslint-disable-line no-console
// kills the process after the replica set is setup
if (stringChunk.includes('Started replica set')) {
if (stringChunk.includes('mongod startup complete')) {
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
runRsProcess.kill();
dockerMongoProcess.kill();
manuallyStopped = true;
}
}
let error = '';
for await (const chunk of runRsProcess.stderr) {
for await (const chunk of dockerMongoProcess.stderr) {
const stringChunk = chunk.toString();
error += stringChunk;
}
const exitCode = await new Promise(resolve => {
runRsProcess.on('close', resolve);
dockerMongoProcess.on('close', resolve);
});
if (exitCode || error.length > 0) {
if (!manuallyStopped && (exitCode || error.length > 0)) {
// remove any leftover files
clean.sync(MONGO_PATH);
+5
View File
@@ -53,6 +53,11 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
console.info({
mongooseOptions,
connectionUrl,
});
mongoose.connect(connectionUrl, mongooseOptions)
.then(() => mongoose.connection.dropDatabase())
.then(() => mongoose.connection.close()).then(() => {
+279 -708
View File
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.44.2",
"version": "5.47.4",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -39,9 +39,9 @@
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"habitica-markdown": "^4.1.0",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"helmet": "^8.1.0",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.2",
@@ -76,7 +76,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"
@@ -103,13 +102,16 @@
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:dev:docker": "cd website/client && npm run serve:docker",
"client:build": "cd website/client && npm run build",
"client:unit": "cd website/client && npm run test:unit",
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "node --watch --inspect ./website/server/index.js",
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"docker:aio": "docker compose -f docker-compose.yml up",
"docker:mongo:dev": "docker compose -f docker-compose.mongo-only.yml up",
"docker:mongo:dev:down": "docker compose -f docker-compose.mongo-only.yml down",
"docker:mongo:test": "docker compose -f docker-compose.mongo-test-local.yml up",
"mongo:test": "node scripts/start-local-mongo.mjs --test-db",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
@@ -125,7 +127,6 @@
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
@@ -66,13 +66,15 @@ describe('Amazon Payments - Cancel Subscription', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
user.guilds.push(group._id);
await user.save();
subscriptionBlock = common.content.subscriptionBlocks[subKey];
subscriptionLength = subscriptionBlock.months * 30;
@@ -30,12 +30,14 @@ describe('Amazon Payments - Subscribe', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
user.guilds.push(group._id);
await user.save();
amount = common.content.subscriptionBlocks[subKey].price;
billingAgreementId = 'billingAgreementId';
@@ -246,11 +248,6 @@ describe('Amazon Payments - Subscribe', () => {
user.guilds.push(groupId);
await user.save();
// Add existing users
user = new User();
user.guilds.push(groupId);
await user.save();
// Set expected amount
sub.key = 'group_monthly';
sub.price = 9;
@@ -128,11 +128,12 @@ describe('Purchasing a group plan for group', () => {
expect(publicGroup.purchased.plan.planId).to.not.exist;
data.groupId = publicGroup._id;
// Public Guilds are no longer even findable
await expect(api.createSubscription(data))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: i18n.t('onlyPrivateGuildsCanUpgrade'),
httpCode: 404,
name: 'NotFound',
message: i18n.t('groupNotFound'),
});
const updatedGroup = await Group.findById(publicGroup._id).exec();
@@ -30,13 +30,15 @@ describe('paypal - subscribeCancel', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
group.purchased.plan.customerId = groupCustomerId;
group.purchased.plan.planId = subKey;
group.purchased.plan.lastBillingDate = new Date();
await group.save();
user.guilds.push(group._id);
await user.save();
nextBillingDate = new Date();
@@ -236,7 +236,7 @@ describe('Stripe - Checkout', () => {
const group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
const groupId = group._id;
@@ -376,11 +376,13 @@ describe('Stripe - Checkout', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
groupId = group._id;
await group.save();
user.guilds.push(group._id);
await user.save();
});
it('throws if user is not allowed to change group plan', async () => {
@@ -136,7 +136,7 @@ describe('Stripe - Subscriptions', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
groupId = group._id;
@@ -315,12 +315,14 @@ describe('Stripe - Subscriptions', () => {
group = generateGroup({
name: 'test group',
type: 'guild',
privacy: 'public',
privacy: 'private',
leader: user._id,
});
group.purchased.plan.customerId = 'customer-id';
group.purchased.plan.planId = subKey;
await group.save();
user.guilds.push(group._id);
await user.save();
groupId = group._id;
});
@@ -50,5 +50,59 @@ describe('UserNotification Model', () => {
expect(safeNotifications[0].type).to.equal('NEW_CHAT_MESSAGE');
expect(safeNotifications[0].id).to.equal('123');
});
it('removes duplicate STREAK_ACHIEVEMENT notifications', () => {
// Fixes issue #13325 - Users receiving duplicate streak achievement notifications
const notifications = [
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 123,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 456,
data: {},
}),
new UserNotification({
type: 'CRON',
id: 789,
data: {},
}), // different type, should be kept
];
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
expect(safeNotifications.length).to.equal(2);
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
expect(safeNotifications[0].id).to.equal('123');
expect(safeNotifications[1].type).to.equal('CRON');
expect(safeNotifications[1].id).to.equal('789');
});
it('handles multiple STREAK_ACHIEVEMENT duplicates correctly', () => {
// Test case: 3 duplicate STREAK_ACHIEVEMENT notifications
const notifications = [
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 111,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 222,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 333,
data: {},
}),
];
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
expect(safeNotifications.length).to.equal(1);
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
expect(safeNotifications[0].id).to.equal('111'); // Keep first one
});
});
});
@@ -5,6 +5,8 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('POST /challenges/:challengeId/join', () => {
it('returns error when challengeId is not a valid UUID', async () => {
@@ -27,6 +29,37 @@ describe('POST /challenges/:challengeId/join', () => {
});
});
context('public Guild', () => {
let group;
let groupLeader;
let members;
let challenge;
before(async () => {
({ group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
}));
challenge = await generateChallenge(groupLeader, group);
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
});
it('returns error when challengeId is in an old public Guild', async () => {
const authorizedUser = members[0]; // eslint-disable-line prefer-destructuring
await expect(authorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
});
context('Joining a valid challenge', () => {
let groupLeader;
let group;
@@ -66,6 +99,15 @@ describe('POST /challenges/:challengeId/join', () => {
expect(res.name).to.equal(challenge.name);
});
it('succeeds when it\'s a Tavern challenge, even if the user isn\'t a "member" of Tavern', async () => {
const tavern = await groupLeader.get(`/groups/${TAVERN_ID}`);
const tavernChallenge = await generateChallenge(groupLeader, tavern, { prize: 1 });
const generalUser = await generateUser();
const res = await generalUser.post(`/challenges/${tavernChallenge._id}/join`);
expect(res.name).to.equal(tavernChallenge.name);
});
it('returns challenge data', async () => {
const res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
@@ -62,9 +62,9 @@ describe('GET /groups/:groupId/chat', () => {
it('returns error if user attempts to fetch a sunset Guild', async () => {
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
});
@@ -121,9 +121,9 @@ describe('POST /chat/:chatId/like', () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
});
@@ -1,35 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('GET /export/avatar-:memberId.html', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
const dummyId = generateUUID();
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: dummyId }),
});
});
it('returns an html page', async () => {
const res = await user.get(`/export/avatar-${user._id}.html`);
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
});
});
@@ -1,3 +0,0 @@
// TODO how to test this route since it points to a file on AWS s3?
describe('GET /export/avatar-:memberId.png', () => {});
@@ -38,7 +38,7 @@ describe('GET /export/inbox.html', () => {
it('renders the markdown messages as html', async () => {
const res = await user.get('/export/inbox.html');
expect(res).to.include('img class="habitica-emoji"');
expect(res).to.include('😄');
expect(res).to.include('<h1>Hello!</h1>');
expect(res).to.include('<li>list 1</li>');
});
@@ -46,7 +46,7 @@ describe('GET /export/inbox.html', () => {
it('sorts messages from newest to oldest', async () => {
const res = await user.get('/export/inbox.html');
const emojiPosition = res.indexOf('img class="habitica-emoji"');
const emojiPosition = res.indexOf('😄');
const headingPosition = res.indexOf('<h1>Hello!</h1>');
const listPosition = res.indexOf('<li>list 1</li>');
+67
View File
@@ -0,0 +1,67 @@
import md from 'habitica-markdown';
describe('habiticaMarkdown emoji plugin', () => {
it('renders standard emoji as Unicode', () => {
const result = md.render(':smile:');
expect(result).to.include('😄');
expect(result).not.to.include('img');
});
it('renders thumbsup emoji as Unicode', () => {
const result = md.render(':thumbsup:');
expect(result).to.include('👍');
});
it('renders +1 emoji as Unicode', () => {
const result = md.render(':+1:');
expect(result).to.include('👍');
});
it('renders melior as an img tag', () => {
const result = md.render(':melior:');
expect(result).to.include('<img class="habitica-emoji"');
expect(result).to.include('src="https://s3.amazonaws.com/habitica-assets/cdn/emoji/melior.png"');
expect(result).to.include('alt="melior"');
});
it('does NOT convert emoji inside markdown links', () => {
const result = md.render('[:smile: link](http://example.com)');
expect(result).to.include(':smile: link');
expect(result).not.to.include('😄');
});
it('converts emoji outside of links normally', () => {
const result = md.render(':smile: [link](http://example.com)');
expect(result).to.include('😄');
expect(result).to.include('link');
});
it('leaves removed custom emoji (bowtie) as literal text', () => {
const result = md.render(':bowtie:');
expect(result).to.include(':bowtie:');
expect(result).not.to.include('img');
});
it('leaves unknown shortcodes as literal text', () => {
const result = md.render(':nonexistent_emoji_xyz:');
expect(result).to.include(':nonexistent_emoji_xyz:');
});
it('renders new emoji not in the old dataset', () => {
const result = md.render(':yawning_face:');
expect(result).to.include('🥱');
});
it('supports unsafeHTMLRender', () => {
const result = md.unsafeHTMLRender('<b>bold</b> :smile:');
expect(result).to.include('<b>bold</b>');
expect(result).to.include('😄');
});
it('supports renderWithMentions', () => {
const result = md.renderWithMentions(':smile: @testuser', { userName: 'testuser' });
expect(result).to.include('😄');
expect(result).to.include('at-text');
expect(result).to.include('at-highlight');
});
});
+17 -8
View File
@@ -211,22 +211,32 @@ describe('shared.ops.rebirth', () => {
expect(user.achievements.rebirthLevel).to.equal(2);
});
it('does not increment rebirth achievements when level is lower than previous', async () => {
it('increments rebirth achievements even when level is lower than previous', async () => {
user.stats.lvl = 2;
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = 3;
await rebirth(user);
expect(user.achievements.rebirths).to.equal(1);
expect(user.achievements.rebirths).to.equal(2);
expect(user.achievements.rebirthLevel).to.equal(3);
});
it('always increments rebirth achievements when level is MAX_LEVEL', async () => {
it('updates rebirthLevel when current level is higher than previous', async () => {
user.stats.lvl = 5;
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = 3;
await rebirth(user);
expect(user.achievements.rebirths).to.equal(2);
expect(user.achievements.rebirthLevel).to.equal(5);
});
it('increments rebirth achievements when level is MAX_LEVEL', async () => {
user.stats.lvl = MAX_LEVEL;
user.achievements.rebirths = 1;
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
user.achievements.rebirthLevel = MAX_LEVEL + 1;
user.achievements.rebirthLevel = MAX_LEVEL;
await rebirth(user);
@@ -234,11 +244,10 @@ describe('shared.ops.rebirth', () => {
expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL);
});
it('always increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
it('increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
user.stats.lvl = MAX_LEVEL + 1;
user.achievements.rebirths = 1;
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
user.achievements.rebirthLevel = MAX_LEVEL + 2;
user.achievements.rebirthLevel = MAX_LEVEL;
await rebirth(user);
+41
View File
@@ -0,0 +1,41 @@
import { getMatchingSwap, makeSubstitutionMap } from '../../website/common/script/content/constants/aprilFools';
describe('April Fools', () => {
describe('getMatchingSwap', () => {
it('returns Veggie for 2020', () => {
const swap = getMatchingSwap(new Date('2020-04-01'));
expect(swap).to.equal('Veggie');
});
it('returns Alien for 2026', () => {
const swap = getMatchingSwap(new Date('2026-04-01'));
expect(swap).to.equal('Alien');
});
it('Cycles through swaps correctly', () => {
const swap = getMatchingSwap(new Date('2027-04-01'));
expect(swap).to.equal('Veggie');
});
});
describe('makeSubstitutionMap', () => {
it('returns correct substitution for Veggie', () => {
const substitutions = makeSubstitutionMap('Veggie');
expect(substitutions.pets['Pet-Wolf-']).to.equal('Pet-Wolf-Veggie');
expect(substitutions.pets['Pet-TigerCub-']).to.equal('Pet-TigerCub-Veggie');
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Veggie');
expect(substitutions.pets.default).to.equal('Pet-Dragon-Veggie');
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Veggie');
expect(substitutions.pets.noPetIOS).to.equal('Pet-TigerCub-Veggie');
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Veggie');
});
it('returns correct substitution for Cryptid', () => {
const substitutions = makeSubstitutionMap('Cryptid');
expect(substitutions.pets['Pet-Fox-']).to.equal('Pet-Fox-Cryptid');
expect(substitutions.pets['Pet-FlyingPig-']).to.equal('Pet-FlyingPig-Cryptid');
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Cryptid');
expect(substitutions.pets.default).to.equal('Pet-Dragon-Cryptid');
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Cryptid');
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Cryptid');
});
});
});
+1
View File
@@ -21,6 +21,7 @@ export async function getProperty (collectionName, id, path) {
// Specifically helpful for the GET /groups tests,
// resets the db to an empty state and creates a tavern document
export async function resetHabiticaDB () {
console.info('Resetting Habitica DB');
const groups = mongoose.connection.db.collection('groups');
const users = mongoose.connection.db.collection('users');
return mongoose.connection.dropDatabase()
-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',
+4655 -3035
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"serve": "vite",
"serve:docker": "npx vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run",
@@ -27,7 +28,7 @@
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
"eslint-plugin-vue": "7.20.0",
"habitica-markdown": "^3.0.0",
"habitica-markdown": "^4.0.0",
"hellojs": "^1.20.0",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
@@ -45,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",
@@ -59,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"
}
}
+5
View File
@@ -229,6 +229,11 @@ export default {
}
return Promise.resolve(error);
}
if (error.response.status === 404
&& error.response.config.method === 'get'
&& error.response.config.url.indexOf('/api/v4/groups/party') !== -1) {
return Promise.reject(error);
}
}
const errorData = error.response.data;
+12 -1
View File
@@ -22,8 +22,15 @@
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_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
.Pet_HatchingPotion_Alien {
width: 68px;
height: 68px;
}
@@ -52,6 +59,10 @@
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;
@@ -1060,6 +1060,11 @@
width: 141px;
height: 147px;
}
.background_elven_citadel {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elven_citadel.png');
width: 141px;
height: 147px;
}
.background_enchanted_music_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png');
width: 141px;
@@ -1796,6 +1801,11 @@
width: 141px;
height: 147px;
}
.background_on_a_strange_planet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_strange_planet.png');
width: 141px;
height: 147px;
}
.background_on_tree_branch {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
width: 141px;
@@ -1931,6 +1941,11 @@
width: 141px;
height: 147px;
}
.background_riding_a_comet {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_riding_a_comet.png');
width: 141px;
height: 147px;
}
.background_rime_ice {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rime_ice.png');
width: 141px;
@@ -2427,6 +2442,11 @@
width: 141px;
height: 147px;
}
.background_waterfall_with_rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_waterfall_with_rainbow.png');
width: 141px;
height: 147px;
}
.background_wedding_arch {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_wedding_arch.png');
width: 141px;
@@ -29800,6 +29820,11 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_handstandOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_handstandOutfit.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_hattersSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_hattersSuit.png');
width: 114px;
@@ -30075,6 +30100,11 @@
width: 114px;
height: 90px;
}
.broad_armor_armoire_softYellowSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softYellowSuit.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_springPetalYukata {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_springPetalYukata.png');
width: 114px;
@@ -30385,6 +30415,11 @@
width: 114px;
height: 90px;
}
.head_armoire_floppyYellowHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyYellowHat.png');
width: 114px;
height: 90px;
}
.head_armoire_flutteryWig {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png');
width: 114px;
@@ -30705,6 +30740,11 @@
width: 114px;
height: 90px;
}
.head_armoire_verdantArmingCap {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_verdantArmingCap.png');
width: 114px;
height: 90px;
}
.head_armoire_vermilionArcherHelm {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_vermilionArcherHelm.png');
width: 90px;
@@ -31120,6 +31160,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_softYellowPillow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softYellowPillow.png');
width: 114px;
height: 90px;
}
.shield_armoire_spanishGuitar {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png');
width: 114px;
@@ -31170,6 +31215,11 @@
width: 114px;
height: 90px;
}
.shield_armoire_verdantBanner {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_verdantBanner.png');
width: 114px;
height: 90px;
}
.shield_armoire_vikingShield {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_vikingShield.png');
width: 90px;
@@ -31440,6 +31490,11 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_handstandOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_hattersSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_hattersSuit.png');
width: 114px;
@@ -31715,6 +31770,11 @@
width: 114px;
height: 90px;
}
.slim_armor_armoire_softYellowSuit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softYellowSuit.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_springPetalYukata {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_springPetalYukata.png');
width: 114px;
@@ -34125,11 +34185,21 @@
width: 114px;
height: 90px;
}
.back_mystery_202605 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202605.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_202604 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202604.png');
width: 114px;
height: 90px;
}
.head_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
width: 114px;
@@ -34140,11 +34210,31 @@
width: 114px;
height: 90px;
}
.head_mystery_202603 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202603.png');
width: 114px;
height: 90px;
}
.head_mystery_202604 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202604.png');
width: 114px;
height: 90px;
}
.shield_mystery_202605 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
width: 114px;
height: 90px;
}
.slim_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
width: 114px;
height: 90px;
}
.slim_armor_mystery_202604 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202604.png');
width: 114px;
height: 90px;
}
.weapon_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png');
width: 114px;
@@ -34155,6 +34245,11 @@
width: 114px;
height: 90px;
}
.weapon_mystery_202603 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202603.png');
width: 114px;
height: 90px;
}
.back_mystery_201402 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
width: 90px;
@@ -36275,6 +36370,26 @@
width: 114px;
height: 90px;
}
.broad_armor_special_spring2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_spring2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Mage.png');
width: 114px;
height: 90px;
}
.broad_armor_special_spring2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Rogue.png');
width: 114px;
height: 90px;
}
.broad_armor_special_spring2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_springHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_springHealer.png');
width: 90px;
@@ -36595,6 +36710,26 @@
width: 114px;
height: 90px;
}
.head_special_spring2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Healer.png');
width: 114px;
height: 90px;
}
.head_special_spring2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Mage.png');
width: 114px;
height: 90px;
}
.head_special_spring2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Rogue.png');
width: 114px;
height: 90px;
}
.head_special_spring2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Warrior.png');
width: 114px;
height: 90px;
}
.head_special_springHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_springHealer.png');
width: 90px;
@@ -36780,6 +36915,21 @@
width: 114px;
height: 90px;
}
.shield_special_spring2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Healer.png');
width: 114px;
height: 90px;
}
.shield_special_spring2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Rogue.png');
width: 114px;
height: 90px;
}
.shield_special_spring2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_springHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_springHealer.png');
width: 90px;
@@ -37015,6 +37165,26 @@
width: 114px;
height: 90px;
}
.slim_armor_special_spring2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_spring2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Mage.png');
width: 114px;
height: 90px;
}
.slim_armor_special_spring2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Rogue.png');
width: 114px;
height: 90px;
}
.slim_armor_special_spring2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_springHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_springHealer.png');
width: 90px;
@@ -37255,6 +37425,26 @@
width: 114px;
height: 90px;
}
.weapon_special_spring2026Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_spring2026Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Mage.png');
width: 114px;
height: 90px;
}
.weapon_special_spring2026Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Rogue.png');
width: 114px;
height: 90px;
}
.weapon_special_spring2026Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_springHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_springHealer.png');
width: 90px;
@@ -53038,6 +53228,11 @@
width: 81px;
height: 99px;
}
.Pet-BearCub-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Alien.png');
width: 81px;
height: 99px;
}
.Pet-BearCub-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Amber.png');
width: 81px;
@@ -53528,6 +53723,11 @@
width: 81px;
height: 99px;
}
.Pet-Cactus-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Alien.png');
width: 81px;
height: 99px;
}
.Pet-Cactus-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Amber.png');
width: 81px;
@@ -54318,6 +54518,11 @@
width: 81px;
height: 99px;
}
.Pet-Dragon-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Alien.png');
width: 81px;
height: 99px;
}
.Pet-Dragon-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Amber.png');
width: 81px;
@@ -54813,6 +55018,11 @@
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Alien.png');
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Amber.png');
width: 81px;
@@ -55148,6 +55358,11 @@
width: 81px;
height: 99px;
}
.Pet-Fox-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Alien.png');
width: 81px;
height: 99px;
}
.Pet-Fox-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Amber.png');
width: 81px;
@@ -55928,6 +56143,11 @@
width: 81px;
height: 99px;
}
.Pet-LionCub-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Alien.png');
width: 81px;
height: 99px;
}
.Pet-LionCub-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Amber.png');
width: 81px;
@@ -56533,6 +56753,11 @@
width: 81px;
height: 99px;
}
.Pet-PandaCub-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Alien.png');
width: 81px;
height: 99px;
}
.Pet-PandaCub-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Amber.png');
width: 81px;
@@ -57928,6 +58153,11 @@
width: 81px;
height: 99px;
}
.Pet-TigerCub-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Alien.png');
width: 81px;
height: 99px;
}
.Pet-TigerCub-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Amber.png');
width: 81px;
@@ -58573,6 +58803,11 @@
width: 81px;
height: 99px;
}
.Pet-Wolf-Alien {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Alien.png');
width: 81px;
height: 99px;
}
.Pet-Wolf-Amber {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Amber.png');
width: 81px;
Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

@@ -58,6 +58,11 @@ h3.markdown {
img {
max-width: 100%;
}
.emoji-native {
font-size: 0.85em;
vertical-align: middle;
}
blockquote {
padding: 0 16px;
+4
View File
@@ -16,6 +16,10 @@
border-bottom: 0;
}
.d-content {
display: contents;
}
* {
transition: none;
}
@@ -0,0 +1,5 @@
<svg width="330" height="80" viewBox="0 0 330 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.797 60.5502C165.534 61.8466 171.631 62.7536 178.221 63.1767C208.94 65.1492 233.733 56.6838 260.68 47.483C282.33 40.091 305.37 32.2243 333.99 28.9144L336 16.3018C260.81 7.08865 233.373 23.1672 205.362 39.5825C192.037 47.3908 178.583 55.2753 159.797 60.5502Z" fill="#BDA8FF"/>
<path d="M0 80L331.948 79.9998V29.1594C268.976 36.9871 233.03 66.6959 178.221 63.1767C112.951 58.9858 95.9516 7.31934 0.000104656 0L0 80Z" fill="#925CF3"/>
<path d="M203.54 40.6496C166.339 36.8525 141.531 39.6251 122.334 45.4666C133.94 51.8989 145.792 57.3851 159.797 60.5502C177.727 55.5155 190.801 48.1036 203.54 40.6496Z" fill="#D5C8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

@@ -1,53 +1,228 @@
<template>
<b-modal
id="rebirth"
:title="$t('modalAchievement')"
size="md"
:hide-footer="true"
size="sm"
:hide-header="true"
>
<div class="modal-body">
<div class="col-12">
<!-- @TODO: +achievementAvatar('sun',0)--><achievement-avatar class="avatar" />
</div><div class="col-6 offset-3 text-center">
<div v-if="user.achievements.rebirthLevel < 100">
{{ $t('rebirthAchievement', {
number: user.achievements.rebirths,
level: user.achievements.rebirthLevel}) }}
</div><div v-if="user.achievements.rebirthLevel >= 100">
{{ $t('rebirthAchievement100', {number: user.achievements.rebirths}) }}
</div><br><button
class="btn btn-primary"
@click="close()"
>
{{ $t('huzzah') }}
</button>
<div
class="close-x"
@click.stop="close()"
>
<div
class="svg-icon svg-close"
v-html="icons.close"
></div>
</div>
<div class="content text-center">
<h2
v-once
class="header"
>
{{ $t('rebirthNewAchievement') }}
</h2>
<div class="d-flex align-items-center justify-content-center icon-area">
<div
v-once
class="svg-icon sparkles mirror"
v-html="icons.starGroup"
></div>
<Sprite
class="achievement-icon"
image-name="achievement-sun2x"
/>
<div
v-once
class="svg-icon sparkles"
v-html="icons.starGroup"
></div>
</div>
</div><achievement-footer />
<p class="subtitle">
{{ $t('rebirthNewAdventure') }}
</p>
<p
class="description"
v-html="achievementText"
></p>
<p
v-once
class="stack-info"
>
{{ $t('rebirthStackInfo') }}
</p>
<button
v-once
class="btn btn-primary"
@click="close()"
>
{{ $t('onwards') }}
</button>
</div>
<div
slot="modal-footer"
class="footer-wave"
v-html="icons.purpleWaves"
></div>
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
<style lang="scss">
@import '@/assets/scss/colors.scss';
#rebirth {
.modal-dialog {
width: 330px;
}
.modal-content {
border: none;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-body {
padding: 0;
}
.modal-footer {
padding: 0;
border-top: none;
border-radius: 0;
margin: 0;
line-height: 0;
}
}
</style>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.content {
padding: 24px 24px 0;
}
.header {
font-size: 1.25rem;
line-height: 1.4;
color: $purple-200;
margin-top: 8px;
margin-bottom: 16px;
}
.icon-area {
margin-bottom: 16px;
}
.sparkles {
width: 40px;
height: 64px;
&.mirror {
transform: scaleX(-1);
}
}
.close-x {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
z-index: 2;
&:hover .svg-close {
opacity: 0.75;
}
.svg-close {
width: 16px;
height: 16px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
}
.achievement-icon {
margin: 0 24px;
}
.subtitle {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
margin-bottom: 12px;
color: $gray-10;
}
.description {
font-size: 0.875rem;
line-height: 1.71;
margin-bottom: 12px;
color: $gray-50;
}
.stack-info {
font-size: 0.875rem;
line-height: 1.71;
color: $gray-50;
margin-bottom: 24px;
}
.btn-primary {
margin-bottom: 8px;
}
.footer-wave {
width: 100%;
::v-deep svg {
display: block;
width: calc(100% + 8px);
height: auto;
margin: 0 -4px -4px;
}
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import closeIcon from '@/assets/svg/close.svg?raw';
import Sprite from '@/components/ui/sprite';
import starGroup from '@/assets/svg/star-group.svg?raw';
import purpleWaves from '@/assets/svg/purple-waves.svg?raw';
import { mapState } from '@/libs/store';
export default {
components: {
achievementFooter,
achievementAvatar,
Sprite,
},
data () {
return {
icons: Object.freeze({
starGroup,
purpleWaves,
close: closeIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
achievementText () {
const rebirths = this.user.achievements.rebirths || 0;
const level = this.user.achievements.rebirthLevel || 0;
if (level >= 100) {
return this.$t('rebirthAchievement100', { number: rebirths, level });
}
if (rebirths === 1) {
return this.$t('rebirthAchievement', { number: rebirths, level });
}
return this.$t('rebirthAchievementPlural', { number: rebirths, level });
},
},
methods: {
close () {
@@ -1,41 +1,186 @@
<template>
<b-modal
id="rebirth-enabled"
:title="$t('rebirthNew')"
size="md"
:hide-footer="true"
size="sm"
:hide-header="true"
>
<div class="modal-body">
<div class="col-12">
<div class="rebirth_orb"></div>
<p>
<span>{{ $t('rebirthUnlock') }}</span>
</p>
</div>
<div
class="close-x"
@click.stop="close()"
>
<div
class="svg-icon svg-close"
v-html="icons.close"
></div>
</div>
<div class="modal-footer">
<div class="col-12 text-center">
<button
class="btn btn-primary"
@click="close()"
<div class="content text-center">
<h2
v-once
class="header"
>
{{ $t('rebirthUnlockedNewItem') }}
</h2>
<div class="d-flex align-items-center justify-content-center icon-area">
<div
v-once
class="svg-icon sparkles mirror"
v-html="icons.starGroup"
></div>
<img
class="orb-icon"
src="@/assets/images/rebirth-orb.png"
alt="Orb of Rebirth"
>
{{ $t('close') }}
</button>
<div
v-once
class="svg-icon sparkles"
v-html="icons.starGroup"
></div>
</div>
<p
v-once
class="subtitle"
>
{{ $t('rebirthUnlockedOrb') }}
</p>
<p
v-once
class="description"
>
{{ $t('rebirthUnlockedDesc') }}
</p>
<button
v-once
class="btn btn-primary"
@click="close()"
>
{{ $t('onwards') }}
</button>
</div>
<div
slot="modal-footer"
class="clearfix"
></div>
</b-modal>
</template>
<style scoped>
.rebirth_orb {
margin: 0 auto;
<style lang="scss">
@import '@/assets/scss/colors.scss';
#rebirth-enabled {
.modal-dialog {
width: 330px;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-body {
padding: 0;
}
.modal-footer {
padding: 0;
border-top: none;
}
}
</style>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.content {
padding: 24px 24px 0;
}
.header {
font-size: 1.25rem;
line-height: 1.4;
color: $purple-200;
margin-top: 8px;
margin-bottom: 12px;
}
.icon-area {
margin-bottom: 12px;
}
.sparkles {
width: 40px;
height: 64px;
&.mirror {
transform: scaleX(-1);
}
}
.close-x {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
z-index: 2;
&:hover .svg-close {
opacity: 0.75;
}
.svg-close {
width: 16px;
height: 16px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
}
.orb-icon {
width: 62px;
height: 62px;
margin: 0 24px;
image-rendering: pixelated;
}
.subtitle {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
margin-bottom: 12px;
color: $gray-50;
}
.description {
font-size: 0.875rem;
line-height: 1.71;
margin-bottom: 24px;
color: $gray-100;
}
.btn-primary {
margin-bottom: 24px;
}
</style>
<script>
import closeIcon from '@/assets/svg/close.svg?raw';
import starGroup from '@/assets/svg/star-group.svg?raw';
import { mapState } from '@/libs/store';
export default {
data () {
return {
icons: Object.freeze({
starGroup,
close: closeIcon,
}),
};
},
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;
@@ -609,7 +609,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: {
@@ -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"
@@ -58,8 +81,9 @@
></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,19 @@
<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,
};
},
computed: {
@@ -183,30 +205,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 +269,7 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'),
username: this.username,
email: this.email,
allowRegister: true,
});
} else {
+5 -4
View File
@@ -321,10 +321,11 @@ export default {
return null;
},
petClass () {
const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment()
.isBetween(event.start, event.end));
if (foolEvent) {
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
const substitutionEvent = this.currentEventList?.find(event => event.spriteSubstitutions
&& moment().isBetween(event.start, event.end));
if (substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
return this.foolPet(`Pet-${this.member.items.currentPet}`,
substitutionEvent.spriteSubstitutions.pets);
}
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
return '';
@@ -12,23 +12,39 @@
<label>
<strong v-once>{{ $t('name') }} *</strong>
</label>
<b-form-input
<input
ref="nameInput"
v-model="workingChallenge.name"
class="form-control"
type="text"
:placeholder="$t('challengeNamePlaceholder')"
@keydown="enableSubmit"
/>
@focus="setActiveField('name')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
<div class="form-group">
<label>
<strong v-once>{{ $t('shortName') }} *</strong>
</label>
<b-form-input
<input
ref="shortNameInput"
v-model="workingChallenge.shortName"
class="form-control"
type="text"
:placeholder="$t('shortNamePlaceholder')"
@keydown="enableSubmit"
/>
@focus="setActiveField('shortName')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
<div class="form-group">
<label>
@@ -40,10 +56,17 @@
{{ $t('charactersRemaining', {characters: charactersRemaining}) }}
</div>
<textarea
ref="summaryTextarea"
v-model="workingChallenge.summary"
class="summary-textarea form-control"
:placeholder="$t('challengeSummaryPlaceholder')"
@keydown="enableSubmit"
@focus="setActiveField('summary')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
</div>
<div class="form-group">
@@ -55,11 +78,26 @@
class="float-right"
></a>
<textarea
ref="descriptionTextarea"
v-model="workingChallenge.description"
class="description-textarea form-control"
:placeholder="$t('challengeDescriptionPlaceholder')"
@keydown="enableSubmit"
@focus="setActiveField('description')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<div
v-if="creating"
@@ -280,12 +318,17 @@ import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHAL
import CategoryOptions from '@/../../common/script/content/categoryOptions';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
components: {
emojiAutoComplete,
},
directives: {
markdown: markdownDirective,
},
mixins: [userStateMixin],
mixins: [userStateMixin, autoCompleteHelperMixin],
props: ['groupId'],
data () {
const categoryOptions = CategoryOptions;
@@ -319,9 +362,14 @@ export default {
categoriesHashByKey,
loading: false,
groups: [],
textbox: null,
activeField: 'name',
};
},
computed: {
activeFieldText () {
return this.workingChallenge[this.activeField] || '';
},
creating () {
return !this.workingChallenge.id;
},
@@ -589,6 +637,29 @@ export default {
toggleCategorySelect () {
this.showCategorySelect = !this.showCategorySelect;
},
setActiveField (field) {
this.activeField = field;
const refMap = {
name: 'nameInput',
shortName: 'shortNameInput',
summary: 'summaryTextarea',
description: 'descriptionTextarea',
};
this.textbox = this.$refs[refMap[field]] || null;
},
onFieldKeydown (e) {
this.enableSubmit();
this.autoCompleteMixinUpdateCarretPosition(e);
},
selectedAutocomplete (newText, newCaret) {
this.workingChallenge[this.activeField] = newText;
this.$nextTick(() => {
if (this.textbox) {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
}
});
},
enableSubmit: throttle(function enableSubmit () {
/* Enables the submit button if it was disabled */
if (this.loading) {
@@ -0,0 +1,282 @@
<template>
<div
v-if="searchResults.length > 0"
class="autocomplete-selection"
:style="autocompleteStyle"
>
<div
v-for="result in searchResults"
:key="result.shortcode"
class="autocomplete-results d-flex align-items-center"
:class="{'hover-background': result.hover}"
@click="select(result)"
@mouseenter="setHover(result)"
@mouseleave="resetSelection()"
>
<img
v-if="result.imageUrl"
class="emoji-img"
:src="result.imageUrl"
:alt="result.shortcode"
>
<span
v-else
class="emoji-char"
>{{ result.emoji }}</span>
<span
class="shortcode ml-2"
:class="{'hover-foreground': result.hover}"
>:{{ result.shortcode }}:</span>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.autocomplete-results {
padding: .5em;
}
.autocomplete-selection {
box-shadow: 1px 1px 1px #efefef;
}
.hover-background {
background-color: rgba(213, 200, 255, 0.32);
cursor: pointer;
}
.hover-foreground {
color: $purple-300 !important;
}
.emoji-char {
font-size: 20px;
line-height: 1;
}
.emoji-img {
height: 20px;
width: 20px;
}
.shortcode {
color: $gray-200;
font-size: 14px;
}
</style>
<script>
import habiticaMarkdown from 'habitica-markdown';
export default {
props: ['text', 'caretPosition', 'coords', 'textbox'],
data () {
return {
colonRegex: /:([a-zA-Z0-9_+]*)$/,
currentSearch: '',
searchActive: false,
searchResults: [],
selected: null,
emojiList: [],
renderTick: 0,
internalCoords: { TOP: 0, LEFT: 0 },
};
},
computed: {
autocompleteStyle () {
// eslint-disable-next-line no-unused-vars
const _tick = this.renderTick;
const isTextarea = this.textbox.tagName === 'TEXTAREA';
const dropdownPA = (this.$el && this.$el.nodeType === 1) ? this.$el.offsetParent : null;
const textboxOP = this.textbox.offsetParent;
const needsRectCalc = dropdownPA && textboxOP && dropdownPA !== textboxOP;
let top;
let left;
const caretLeft = this.internalCoords.LEFT - (this.textbox.scrollLeft || 0);
if (needsRectCalc) {
const textboxRect = this.textbox.getBoundingClientRect();
const parentRect = dropdownPA.getBoundingClientRect();
const parentScrollTop = dropdownPA.scrollTop || 0;
if (isTextarea) {
const computedStyle = window.getComputedStyle(this.textbox);
const lineHeight = parseFloat(computedStyle.lineHeight)
|| (parseFloat(computedStyle.fontSize) * 1.4);
const caretTopInTextbox = this.internalCoords.TOP
- (this.textbox.scrollTop || 0) + lineHeight;
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
top = (textboxRect.top - parentRect.top) + parentScrollTop + clamped + 2;
} else {
top = (textboxRect.bottom - parentRect.top) + parentScrollTop + 2;
}
left = (textboxRect.left - parentRect.left) + caretLeft;
} else {
if (isTextarea) {
const computedStyle = window.getComputedStyle(this.textbox);
const lineHeight = parseFloat(computedStyle.lineHeight)
|| (parseFloat(computedStyle.fontSize) * 1.4);
const caretTopInTextbox = this.internalCoords.TOP
- (this.textbox.scrollTop || 0) + lineHeight;
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
top = this.textbox.offsetTop + clamped + 2;
} else {
top = this.textbox.offsetTop + this.textbox.offsetHeight + 2;
}
left = this.textbox.offsetLeft + caretLeft;
}
return {
top: `${top}px`,
left: `${left}px`,
position: 'absolute',
minWidth: '150px',
zIndex: 100,
backgroundColor: 'white',
};
},
},
watch: {
searchResults (results, oldResults) {
if (results.length > 0 && (!oldResults || oldResults.length === 0)) {
this.$nextTick(() => {
this.renderTick += 1;
});
}
},
text (newText, prevText) {
if (!this.textbox) return;
this._measureCaretCoords();
const delCharsBool = prevText.length > newText.length;
const caretPosition = this.textbox.selectionEnd;
const lastFocusChar = delCharsBool ? prevText[caretPosition] : newText[caretPosition - 1];
if (
newText.length === 0
|| (lastFocusChar === ':' && delCharsBool)
) {
this.cancel();
} else {
if (lastFocusChar === ':') this.searchActive = true;
if (this.searchActive) {
this.searchResults = this.solveSearchResults(newText.substring(0, caretPosition));
}
}
},
},
created () {
const defs = habiticaMarkdown.emojiDefs;
if (!defs) return;
const customEmojis = habiticaMarkdown.customEmojis || {};
const list = [];
const keys = Object.keys(defs);
keys.sort();
for (const key of keys) {
const entry = { shortcode: key, emoji: defs[key], hover: false };
if (customEmojis[key]) {
entry.imageUrl = customEmojis[key];
}
list.push(entry);
}
this.emojiList = list;
},
methods: {
solveSearchResults (textFocus) {
const regexRes = this.colonRegex.exec(textFocus);
if (!regexRes) {
this.cancel();
return [];
}
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
if (this.currentSearch.length === 0) return [];
const lowerSearch = this.currentSearch.toLowerCase();
return this.emojiList
.filter(entry => entry.shortcode.startsWith(lowerSearch))
.slice(0, 6)
.map(entry => ({ ...entry, hover: false }));
},
select (result) {
const { text } = this;
const targetName = `${result.shortcode}: `;
const oldCaret = this.caretPosition;
const escapedSearch = this.currentSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let newText = text.substring(0, this.caretPosition)
.replace(new RegExp(`${escapedSearch}$`), targetName);
const newCaret = newText.length;
newText += text.substring(oldCaret, text.length);
this.$emit('select', newText, newCaret);
this.cancel();
},
setHover (result) {
this.resetSelection();
result.hover = true;
},
clearHover () {
for (const selection of this.searchResults) {
selection.hover = false;
}
},
resetSelection () {
this.clearHover();
this.selected = null;
},
selectNext () {
if (this.searchResults.length > 0) {
this.clearHover();
this.selected = this.selected === null
? 0
: (this.selected + 1) % this.searchResults.length;
this.searchResults[this.selected].hover = true;
}
},
selectPrevious () {
if (this.searchResults.length > 0) {
this.clearHover();
this.selected = this.selected === null
? this.searchResults.length - 1
: (this.selected - 1 + this.searchResults.length) % this.searchResults.length;
this.searchResults[this.selected].hover = true;
}
},
makeSelection () {
if (this.searchResults.length > 0 && this.selected !== null) {
const result = this.searchResults[this.selected];
this.select(result);
}
},
_measureCaretCoords () {
const el = this.textbox;
const caretPosition = el.selectionEnd;
const div = document.createElement('div');
const span = document.createElement('span');
const copyStyle = getComputedStyle(el);
[].forEach.call(copyStyle, prop => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
div.style.visibility = 'hidden';
document.body.appendChild(div);
div.textContent = el.value.substr(0, caretPosition);
span.textContent = el.value.substr(caretPosition) || '.';
div.appendChild(span);
this.internalCoords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
cancel () {
this.searchActive = false;
this.searchResults = [];
this.resetSelection();
},
},
};
</script>
@@ -187,7 +187,8 @@
</div>
</div>
<div
v-if="user.purchased.background.birthday_bash"
v-if="user.purchased.background.birthday_bash
|| user.purchased.background.on_a_strange_planet"
>
<div
class="row justify-content-center title-row mb-3"
@@ -0,0 +1,577 @@
<template>
<b-modal
id="group-plan-selection"
:hide-footer="true"
:hide-header="true"
size="md"
@show="loadData"
@hide="onHide"
>
<div class="selection-modal">
<div class="modal-header-row">
<h2 class="title">
{{ $t('chooseAnOption') }}
</h2>
<div class="header-actions">
<span
class="cancel-text"
@click="close"
>
{{ $t('cancel') }}
</span>
<button
class="btn btn-primary next-button"
:class="{ disabled: !selectedOption }"
:disabled="!selectedOption"
@click="continueFlow"
>
{{ $t('next') }}
</button>
</div>
</div>
<div
v-if="loading"
class="loading-container"
>
<div class="spinner-border text-secondary"></div>
</div>
<template v-else>
<div
v-if="hasUpgradeableGroups"
class="section-header"
>
{{ $t('upgradeExistingGroup') }}
</div>
<selectable-card
v-for="group in upgradeableGuilds"
:key="group._id"
class="option-card"
:selected="isSelected(group)"
@click="selectOption(group)"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ group.name }}
</div>
<div class="option-members">
{{ formatMemberCount(group.memberCount) }}
</div>
<div class="option-label previously-upgraded">
<div
class="svg-icon sparkle-icon"
v-html="icons.sparkles"
></div>
{{ $t('previouslyUpgradedGroup') }}
</div>
</div>
<div class="option-price">
${{ calculatePrice(group.memberCount) }}.00/mo
</div>
</div>
</selectable-card>
<selectable-card
v-if="upgradeableParty"
class="option-card"
:class="{ 'has-pending-warning': partyPendingInviteCount > 0 }"
:selected="isSelected(upgradeableParty)"
@click="selectOption(upgradeableParty)"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ upgradeableParty.name }}
</div>
<div class="option-members">
{{ formatMemberCount(upgradeableParty.memberCount) }}
<span
v-if="partyPendingInviteCount > 0"
class="pending-count"
>
{{ $t('pendingCount', { count: partyPendingInviteCount }) }}
</span>
</div>
<div
v-if="isPartyPreviouslyUpgraded"
class="option-label previously-upgraded"
>
<div
class="svg-icon sparkle-icon"
v-html="icons.sparkles"
></div>
{{ $t('previouslyUpgradedGroup') }}
</div>
<div
v-else
class="option-label your-party"
>
<div
class="svg-icon member-icon"
v-html="icons.member"
></div>
{{ $t('yourParty') }}
</div>
</div>
<div class="option-price">
${{ calculatePrice(upgradeableParty.memberCount) }}.00/mo
</div>
</div>
<div
v-if="partyPendingInviteCount > 0"
class="pending-warning-banner"
>
<div
class="svg-icon alert-icon"
v-html="icons.alert"
></div>
<span class="warning-text">{{ $t('upgradeCancelsPendingInvites') }}</span>
</div>
</selectable-card>
<div
v-if="hasUpgradeableGroups"
class="or-divider"
>
<div class="divider-line"></div>
<span class="or-text">{{ $t('or') }}</span>
<div class="divider-line"></div>
</div>
<selectable-card
class="option-card create-new"
:selected="selectedOption === 'new'"
@click="selectOption('new')"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ $t('createNewGroup') }}
</div>
<div class="option-description">
{{ $t('inviteOthersForAdditional') }}
<span class="price-highlight">${{ perMemberPrice }}.00</span>
{{ $t('perMember') }}.
</div>
</div>
<div class="option-price">
${{ basePrice }}.00/mo
</div>
</div>
</selectable-card>
<div class="footer-note">
{{ $t('additionalMembersProrated') }}
</div>
</template>
</div>
</b-modal>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.selection-modal {
padding: 24px;
}
.modal-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.title {
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
line-height: 28px;
color: $purple-200;
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
}
.cancel-text {
color: $blue-10;
font-size: 0.875rem;
margin-right: 16px;
cursor: pointer;
}
.next-button {
min-width: 64px;
&.disabled {
background-color: $gray-300;
border-color: $gray-300;
cursor: not-allowed;
}
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.section-header {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
color: $gray-10;
margin-bottom: 12px;
}
.option-card {
margin-bottom: 12px;
::v-deep .option-name {
color: $gray-50;
}
&.selected ::v-deep .option-name {
color: $purple-200;
}
}
.pending-warning-banner {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
background-color: $yellow-50;
border-radius: 0 0 6px 6px;
margin: 16px -16px 0 -16px;
gap: 4px;
.selected & {
margin: 15px -15px 0 -15px;
}
.alert-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
::v-deep path {
fill: $gray-10;
}
}
.warning-text {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-10;
}
}
.option-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-left: 32px;
padding-right: 8px;
}
.option-info {
flex: 1;
}
.option-name {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 24px;
margin-bottom: 4px;
}
.option-members {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
margin-bottom: 8px;
.pending-count {
font-weight: 700;
color: $yellow-5;
}
}
.option-label {
display: flex;
align-items: center;
font-family: 'Roboto', sans-serif;
font-size: 12px;
line-height: 16px;
gap: 4px;
&.previously-upgraded {
font-weight: 700;
color: $blue-10;
}
&.your-party {
font-weight: 700;
color: $gray-100;
}
.svg-icon {
width: 14px;
height: 14px;
}
.sparkle-icon {
color: $blue-10;
}
.member-icon {
color: $gray-100;
::v-deep path {
fill: $gray-100;
stroke: $gray-100;
stroke-width: 0.5px;
}
}
}
.option-description {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
.price-highlight {
font-weight: 700;
}
}
.option-price {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 20px;
line-height: 24px;
color: $purple-200;
white-space: nowrap;
}
.or-divider {
display: flex;
align-items: center;
margin: 20px 0;
.divider-line {
flex: 1;
height: 1px;
background-color: $gray-500;
}
.or-text {
padding: 0 16px;
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: $gray-100;
}
}
.create-new {
.option-name {
margin-bottom: 8px;
}
}
.footer-note {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
text-align: center;
margin-top: 16px;
margin-left: 24px;
margin-right: 24px;
}
</style>
<style lang="scss">
#group-plan-selection {
.modal-dialog {
max-width: 504px;
}
.modal-content {
border-radius: 8px;
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
}
.modal-body {
padding: 0;
}
.option-card.has-pending-warning.selectable-card {
padding-bottom: 0;
}
}
</style>
<script>
import axios from 'axios';
import paymentsMixin from '@/mixins/payments';
import { mapState } from '@/libs/store';
import SelectableCard from '@/components/ui/selectableCard.vue';
import svgSparkles from '@/assets/svg/sparkles.svg?raw';
import svgMember from '@/assets/svg/member-icon.svg?raw';
import svgAlert from '@/assets/svg/for-css/alert.svg?raw';
export default {
components: {
SelectableCard,
},
mixins: [paymentsMixin],
data () {
return {
selectedOption: null,
userGuilds: [],
userParty: null,
activeGroupPlanIds: [],
loading: true,
basePrice: 9,
perMemberPrice: 3,
icons: Object.freeze({
sparkles: svgSparkles,
member: svgMember,
alert: svgAlert,
}),
partyPendingInviteCount: 0,
};
},
computed: {
...mapState({ user: 'user.data' }),
upgradeableGuilds () {
return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false;
const { purchased } = group;
if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
});
},
upgradeableParty () {
if (!this.userParty) return null;
const leaderId = this.userParty.leader?._id || this.userParty.leader;
if (leaderId !== this.user._id) return null;
if (this.activeGroupPlanIds.includes(this.userParty._id)) return null;
return this.userParty;
},
hasUpgradeableGroups () {
return this.upgradeableGuilds.length > 0 || this.upgradeableParty !== null;
},
isPartyPreviouslyUpgraded () {
if (!this.userParty) return false;
const { purchased } = this.userParty;
if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
},
},
methods: {
async loadData () {
this.loading = true;
this.selectedOption = null;
this.partyPendingInviteCount = 0;
try {
const [guildsResponse, partyResponse] = await Promise.all([
axios.get('/api/v4/groups', { params: { type: 'guilds', includeExpiredPlans: 'true' } }),
axios.get('/api/v4/groups/party').catch(() => ({ data: { data: null } })),
]);
this.userGuilds = guildsResponse.data.data || [];
this.userParty = partyResponse.data.data;
if (this.userParty) {
try {
const invitesResponse = await axios.get(`/api/v4/groups/${this.userParty._id}/invites`);
this.partyPendingInviteCount = invitesResponse.data.data?.length || 0;
} catch (e) {
this.partyPendingInviteCount = 0;
}
}
await this.$store.dispatch('guilds:getGroupPlans', true);
const groupPlans = this.$store.state.groupPlans?.data || [];
this.activeGroupPlanIds = groupPlans.map(g => g._id);
} catch (e) {
console.error('Error loading group data:', e);
}
this.loading = false;
this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) {
[this.selectedOption] = this.upgradeableGuilds;
} else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty;
} else {
this.selectedOption = 'new';
}
});
},
selectOption (option) {
this.selectedOption = option;
},
isSelected (group) {
if (!this.selectedOption || this.selectedOption === 'new') return false;
return this.selectedOption._id === group._id;
},
calculatePrice (memberCount) {
return this.basePrice + (this.perMemberPrice * (memberCount - 1));
},
formatMemberCount (count) {
return count === 1 ? this.$t('oneMember') : this.$t('membersCount', { count });
},
continueFlow () {
if (!this.selectedOption) return;
const selection = this.selectedOption;
this.close();
if (selection === 'new') {
this.$root.$emit('bv::show::modal', 'create-group');
} else {
this.stripeGroup({ group: selection, upgrade: true });
}
},
close () {
this.$root.$emit('bv::hide::modal', 'group-plan-selection');
},
onHide () {
this.selectedOption = null;
},
},
};
</script>
@@ -41,6 +41,14 @@
:chat="group.chat"
@select="selectedAutocomplete"
/>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="newMessage"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<community-guidelines />
<div class="row chat-actions">
@@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
import externalLinks from '../../mixins/externalLinks';
import autocomplete from '../chat/autoComplete';
import emojiAutoComplete from '../chat/emojiAutoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessages from '../chat/chatMessages';
import { mapState } from '@/libs/store';
@@ -102,6 +111,7 @@ export default {
},
components: {
autocomplete,
emojiAutoComplete,
communityGuidelines,
chatMessages,
},
+82 -57
View File
@@ -25,53 +25,61 @@
<div class="col-12 col-md-6">
<div class="row icon-row">
<div
class="item-with-icon"
class="item-with-icon p-2"
tabindex="0"
role="button"
@keyup.enter="showMemberModal()"
@click="showMemberModal()"
>
<div
v-if="group.memberCount > 1000"
class="svg-icon shield"
v-html="icons.goldGuildBadgeIcon"
></div>
<div
v-if="group.memberCount > 100 && group.memberCount < 999"
class="svg-icon shield"
v-html="icons.silverGuildBadgeIcon"
></div>
<div
v-if="group.memberCount < 100"
class="svg-icon shield"
v-html="icons.bronzeGuildBadgeIcon"
></div>
<span class="number">{{ group.memberCount | abbrNum }}</span>
<div
v-once
class="member-list label"
>
{{ $t('memberList') }}
<div class="box-content">
<div class="icon-number-row">
<div
v-if="group.memberCount > 1000"
class="svg-icon shield"
v-html="icons.goldGuildBadgeIcon"
></div>
<div
v-if="group.memberCount > 100 && group.memberCount < 999"
class="svg-icon shield"
v-html="icons.silverGuildBadgeIcon"
></div>
<div
v-if="group.memberCount < 100"
class="svg-icon shield"
v-html="icons.bronzeGuildBadgeIcon"
></div>
<span class="number">{{ group.memberCount | abbrNum }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('memberList') }}
</div>
</div>
</div>
<div v-if="!isParty">
<div
class="item-with-icon"
class="item-with-icon p-2"
tabindex="0"
role="button"
@keyup.enter="showGroupGems()"
@click="showGroupGems()"
>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="number">{{ group.balance * 4 }}</span>
<div
v-once
class="label"
>
{{ $t('guildBank') }}
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="number">{{ group.balance * 4 }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('guildBank') }}
</div>
</div>
</div>
</div>
@@ -128,35 +136,57 @@
}
.item-with-icon {
display: inline-block;
border-radius: 2px;
background-color: #ffffff;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: 1em;
text-align: center;
min-width: 120px;
margin-left: 1em;
width: 120px;
height: 76px;
margin-right: 1rem;
text-align: center;
font-size: 20px;
vertical-align: bottom;
overflow: hidden;
position: relative;
&:last-of-type {
margin-left: 0.5rem;
.box-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.svg-icon.shield, .svg-icon.gem {
width: 28px;
height: auto;
margin: 0 auto;
.icon-number-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.1em;
.number {
font-size: 18px;
font-weight: normal;
margin-left: 0.2em;
}
}
.svg-icon {
width: 24px;
height: 24px;
display: inline-block;
vertical-align: bottom;
margin-right: 0.5em;
}
.number {
font-size: 22px;
font-weight: bold;
}
.label {
margin-top: .5em;
.details {
font-size: 11px;
color: $gray-200;
width: 100%;
padding: 0 4px;
line-height: 1.1;
word-break: break-word;
max-height: 2.2em;
overflow: visible;
}
}
@@ -215,11 +245,6 @@
.icon-row {
margin-top: 1em;
justify-content: flex-end;
.number {
font-size: 22px;
font-weight: bold;
}
}
.chat-row {
@@ -182,12 +182,10 @@ export default {
return 'GreyedOut';
},
imageName () {
const foolEvent = this.currentEventList?.find(event => moment()
.isBetween(event.start, event.end) && event.aprilFools);
if (this.isOwned() && foolEvent) {
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
const petString = `${this.item.eggKey}-${this.item.key}`;
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
const substitutionEvent = this.currentEventList?.find(event => moment()
.isBetween(event.start, event.end) && event.spriteSubstitutions);
if (this.isOwned() && substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
return `stable_${this.foolPet(`Pet-${this.item.key}`, substitutionEvent.spriteSubstitutions.pets)}`;
}
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
@@ -10,6 +10,9 @@
>
<div class="modal-body">
<news-content ref="newsContent" />
<close-x
@close="dismissAlert()"
/>
</div>
<div class="modal-footer d-flex align-items-center pb-0">
@@ -30,12 +33,18 @@
</template>
<script>
import { mapState } from '@/libs/store';
import newsContent from './newsContent';
import closeX from '../ui/closeX.vue';
export default {
components: {
closeX,
newsContent,
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
async onShow () {
this.$refs.newsContent.getPosts();
@@ -330,6 +330,7 @@ export default {
handledNotifications,
isInitialLoadComplete: false,
pendingRebirthNotification: null,
lastShownStreakCount: null, // Track last shown streak to prevent duplicates
};
},
computed: {
@@ -726,17 +727,24 @@ export default {
this.$root.$emit('habitica:won-challenge', notification);
break;
case 'REBIRTH_ACHIEVEMENT':
if (localStorage.getItem('show-rebirth-confirmation') === 'true') {
markAsRead = false;
} else if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
if (localStorage.getItem('show-rebirth-confirmation') !== 'true') {
if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
}
}
break;
case 'STREAK_ACHIEVEMENT':
// Client-side deduplication: prevent showing duplicate streak achievements
if (this.lastShownStreakCount === this.user.achievements.streak) {
// Same streak already shown, skip this notification
break;
}
this.lastShownStreakCount = this.user.achievements.streak;
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
this.$root.$emit('bv::show::modal', 'streak');
}, this.user.preferences.suppressModals.streak);
@@ -42,7 +42,7 @@
:hide-class-badge="true"
:with-background="true"
:override-avatar-gear="getAvatarOverrides(item)"
:sprites-margin="'0px auto 0px -24px'"
:sprites-margin="'0px auto 0px -2px'"
/>
</div>
<item
@@ -498,8 +498,13 @@ export default {
await this.triggerGetWorldState();
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
if (this.currentEvent.season === 'valentines') {
this.imageURLs.background = 'url(/static/npc/spring/seasonal_shop_opened_background.png)';
this.imageURLs.npc = 'url(/static/npc/spring/seasonal_shop_opened_npc.png)';
} else {
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
}
},
beforeDestroy () {
this.$root.$off('buyModal::boughtItem');
@@ -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';
}
},
@@ -1,5 +1,6 @@
<template>
<div>
<group-plan-selection-modal />
<group-plan-creation-modal />
<div class="d-flex justify-content-center">
<div
@@ -315,10 +316,12 @@
import { setup as setupPayments } from '@/libs/payments';
import paymentsMixin from '../../mixins/payments';
import GroupPlanCreationModal from '../group-plans/groupPlanCreationModal.vue';
import GroupPlanSelectionModal from '../group-plans/groupPlanSelectionModal.vue';
export default {
components: {
GroupPlanCreationModal,
GroupPlanSelectionModal,
},
mixins: [paymentsMixin],
data () {
@@ -359,7 +362,7 @@ export default {
if (this.upgradingGroup._id) {
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });
}
return this.$root.$emit('bv::show::modal', 'create-group');
return this.$root.$emit('bv::show::modal', 'group-plan-selection');
},
},
};
+3 -19
View File
@@ -348,7 +348,6 @@
import throttle from 'lodash/throttle';
import isEmpty from 'lodash/isEmpty';
import draggable from 'vuedraggable';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import Task from './task';
@@ -482,25 +481,10 @@ export default {
return this.$t('addATask', { type });
},
badgeCount () {
// 0 means the badge will not be shown
// It is shown for the all and due views of dailies
// and for the active and scheduled views of todos.
if (this.type === 'todo' && this.activeFilter.label !== 'complete2') {
return this.taskList.length;
} if (this.type === 'daily') {
if (this.activeFilter.label === 'due') {
return this.taskList.length;
} if (this.activeFilter.label === 'all') {
return this.taskList
.reduce(
(count, t) => (!t.completed
&& shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count),
0,
);
}
if (this.type === 'reward') {
return 0;
}
return 0;
return this.taskList.length;
},
},
watch: {
@@ -48,11 +48,19 @@
/>
<input
:ref="'checklistItem-' + $index"
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableEdit"
:class="summaryClass(item)"
@focus="setActiveItem($index)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
<span
v-if="!disabled && !disableEdit"
@@ -81,15 +89,30 @@
</span>
<input
ref="newChecklistInput"
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@focus="setActiveItem(-1)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="newChecklistEnterHandler($event)"
@keyup.enter="addChecklistItem($event, true)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
@blur="addChecklistItem($event, false)"
>
</div>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</b-collapse>
</div>
</template>
@@ -105,6 +128,8 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
import gripIcon from '@/assets/svg/grip.svg?raw';
import checkbox from '@/components/ui/checkbox';
import lockableLabel from './lockableLabel';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
name: 'Checklist',
@@ -112,7 +137,9 @@ export default {
checkbox,
draggable,
lockableLabel,
emojiAutoComplete,
},
mixins: [autoCompleteHelperMixin],
props: {
disabled: {
type: Boolean,
@@ -133,6 +160,8 @@ export default {
showChecklist: true,
hasPossibilityOfIMEConversion: true,
newChecklistItem: null,
textbox: null,
activeItemIndex: -1,
icons: Object.freeze({
positive: positiveIcon,
destroy: deleteIcon,
@@ -141,6 +170,15 @@ export default {
}),
};
},
computed: {
activeFieldText () {
if (this.activeItemIndex === -1) {
return this.newChecklistItem || '';
}
const item = this.checklist[this.activeItemIndex];
return item ? item.text || '' : '';
},
},
methods: {
summaryClass (item) {
if (!this.disableEdit) return '';
@@ -179,6 +217,40 @@ export default {
this.checklist.splice(i, 1);
this.updateChecklist();
},
setActiveItem (index) {
this.activeItemIndex = index;
if (index === -1) {
this.textbox = this.$refs.newChecklistInput;
} else {
const refArr = this.$refs[`checklistItem-${index}`];
this.textbox = refArr ? refArr[0] || refArr : null;
}
},
newChecklistEnterHandler (e) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
ac.makeSelection();
} else if (ac) {
ac.cancel();
this.setHasPossibilityOfIMEConversion(false);
} else {
this.setHasPossibilityOfIMEConversion(false);
}
},
selectedAutocomplete (newText, newCaret) {
if (this.activeItemIndex === -1) {
this.newChecklistItem = newText;
} else {
this.checklist[this.activeItemIndex].text = newText;
}
this.$nextTick(() => {
if (this.textbox) {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
}
});
},
},
};
</script>
@@ -187,6 +259,7 @@ export default {
@import '@/assets/scss/colors.scss';
.checklist-component {
position: relative;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
@@ -9,12 +9,27 @@
@toggle="openOrClose($event)"
>
<b-dropdown-header>
<div class="mb-2">
<div class="mb-2 search-input-wrapper">
<b-form-input
ref="searchInput"
v-model="search"
type="text"
:placeholder="searchPlaceholder"
@keyup.enter="handleSubmit"
@focus="setTextbox"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keydown.enter="searchEnterHandler($event)"
@keydown.esc="searchEscHandler($event)"
/>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="search"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
@@ -41,7 +56,7 @@
v-if="addNew || availableToSelect.length > 0"
:class="{
'item-group': true,
'add-new': availableToSelect.length === 0 && search !== '',
'add-new': search !== '' && !hasExactMatch,
'scroll': availableToSelect.length > 5
}"
>
@@ -71,7 +86,7 @@
</b-dropdown-item-button>
<div
v-if="addNew"
v-if="addNew && search !== '' && !hasExactMatch"
class="hint"
>
{{ $t('pressEnterToAddTag', { tagName: search }) }}
@@ -94,6 +109,10 @@ $itemHeight: 2rem;
}
.select-multi {
.search-input-wrapper {
position: relative;
}
.dropdown-toggle {
padding-left: 0.75rem;
}
@@ -152,7 +171,8 @@ $itemHeight: 2rem;
max-height: #{5*$itemHeight};
&.add-new {
height: 30px;
min-height: 30px;
height: auto;
.hint {
display: block;
@@ -185,6 +205,8 @@ $itemHeight: 2rem;
import Vue from 'vue';
import MultiList from '@/components/tasks/modal-controls/multiList';
import markdownDirective from '@/directives/markdown';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
directives: {
@@ -192,7 +214,9 @@ export default {
},
components: {
MultiList,
emojiAutoComplete,
},
mixins: [autoCompleteHelperMixin],
props: {
addNew: {
type: Boolean,
@@ -221,6 +245,8 @@ export default {
wasTagAdded: false,
selected: this.selectedItems,
search: '',
textbox: null,
itemsAdded: [],
};
},
computed: {
@@ -248,6 +274,16 @@ export default {
return filteredItems;
},
hasExactMatch () {
const searchTerm = this.search.trim().toLowerCase();
if (!searchTerm) return false;
if (this.itemsAdded.indexOf(searchTerm) !== -1) return true;
if (this.availableToSelect.length === 0) return false;
if (this.availableToSelect[0].name.toLowerCase() === searchTerm) {
return true;
}
return false;
},
},
watch: {
selected () {
@@ -286,6 +322,7 @@ export default {
this.closeSelectPopup();
},
selectItem (item) {
if (!item) return;
this.selectedItems.push(item.id);
this.$emit('toggle', item.id);
this.preventHide = true;
@@ -312,12 +349,51 @@ export default {
this.closeSelectPopup();
}
},
setTextbox () {
const ref = this.$refs.searchInput;
this.textbox = ref ? (ref.$el || ref) : null;
},
searchEnterHandler (e) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
e.stopPropagation();
ac.makeSelection();
} else {
if (ac) ac.cancel();
this.handleSubmit();
}
},
searchEscHandler (e) {
const ac = this._getActiveAutocomplete();
if (ac && ac.searchActive) {
e.preventDefault();
e.stopPropagation();
ac.cancel();
}
},
selectedAutocomplete (newText, newCaret) {
this.search = newText;
this.$nextTick(() => {
if (this.textbox) {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
}
});
},
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
this.$emit('addNew', search);
this.search = '';
// If there is a existing tag
if (this.hasExactMatch) {
this.selectItem(this.availableToSelect[0]);
this.search = '';
} else {
// Creating a new tag as there is no existing tag present
this.$emit('addNew', search);
this.itemsAdded.push(search.toLowerCase());
this.search = '';
}
},
},
};
@@ -70,6 +70,13 @@
spellcheck="true"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
@focus="setActiveField('title')"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="titleEnterHandler($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
<div
@@ -92,11 +99,27 @@
</small>
</div>
<textarea
ref="notesTextarea"
v-model="task.notes"
class="form-control input-notes"
:class="cssClass('input')"
:placeholder="$t('addNotes')"
@focus="setActiveField('notes')"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
</div>
<div
@@ -712,6 +735,7 @@
}
.task-modal-header {
position: relative;
color: $white;
width: 100%;
border-top-left-radius: 8px;
@@ -1160,6 +1184,8 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import selectList from '@/components/ui/selectList';
import syncTask from '../../mixins/syncTask';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
import positiveIcon from '@/assets/svg/positive.svg?raw';
import negativeIcon from '@/assets/svg/negative.svg?raw';
@@ -1182,15 +1208,18 @@ export default {
toggleCheckbox,
lockableLabel,
selectList,
emojiAutoComplete,
},
directives: {
markdown: markdownDirective,
},
mixins: [syncTask],
mixins: [syncTask, autoCompleteHelperMixin],
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {
return {
textbox: null,
activeField: 'title',
showAssignedSelect: false,
newChecklistItem: null,
icons: Object.freeze({
@@ -1314,6 +1343,10 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
activeFieldText () {
if (!this.task) return '';
return this.activeField === 'title' ? (this.task.text || '') : (this.task.notes || '');
},
showStatAssignment () {
return this.task.type !== 'reward'
&& !this.groupId
@@ -1366,7 +1399,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');
@@ -1489,6 +1522,35 @@ export default {
},
focusInput () {
this.$refs.inputToFocus.focus();
this.setActiveField('title');
},
setActiveField (field) {
this.activeField = field;
if (field === 'title') {
this.textbox = this.$refs.inputToFocus;
} else {
this.textbox = this.$refs.notesTextarea;
}
},
titleEnterHandler (e) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
ac.makeSelection();
} else if (ac) {
ac.cancel();
}
},
selectedAutocomplete (newText, newCaret) {
if (this.activeField === 'title') {
this.task.text = newText;
} else {
this.task.notes = newText;
}
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
async addTag (name) {
const tagResult = await this.createTag({ name });
+75 -1
View File
@@ -80,9 +80,17 @@
v-html="icons.drag"
></div>
<input
:ref="'tagInput-' + tagIndex"
v-model="tag.name"
class="tag-edit-input inline-edit-input form-control"
type="text"
@focus="setActiveTag(tagIndex)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
<div
class="input-group-append"
@@ -100,11 +108,18 @@
class="col-6 dragSpace"
>
<input
ref="newTagInput"
v-model="newTag"
class="new-tag-item edit-tag-item inline-edit-input form-control"
type="text"
:placeholder="$t('newTag')"
@keydown.enter="addTag($event, tagsType.key)"
@focus="setActiveTag(-1)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="newTagEnterHandler($event, tagsType.key)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
</draggable>
@@ -134,6 +149,15 @@
</div>
</div>
</div>
<emoji-auto-complete
v-if="editingTags"
ref="emojiAutocomplete"
:text="activeTagText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedTagAutocomplete"
/>
<div class="filter-panel-footer clearfix">
<template v-if="editingTags === true">
<div class="text-center">
@@ -405,6 +429,8 @@ import dragIcon from '@/assets/svg/drag_indicator.svg?raw';
import { mapState, mapActions } from '@/libs/store';
import brokenTaskModal from './brokenTaskModal';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
components: {
@@ -414,10 +440,12 @@ export default {
spells,
brokenTaskModal,
draggable,
emojiAutoComplete,
},
directives: {
markdown,
},
mixins: [autoCompleteHelperMixin],
data () {
return {
columns: ['habit', 'daily', 'todo', 'reward'],
@@ -445,10 +473,19 @@ export default {
newTag: null,
editingTask: null,
creatingTask: null,
textbox: null,
activeTagIndex: -1,
};
},
computed: {
...mapState({ user: 'user.data' }),
activeTagText () {
if (this.activeTagIndex === -1) {
return this.newTag || '';
}
const tag = this.tagsSnap.tags[this.activeTagIndex];
return tag ? tag.name || '' : '';
},
tagsByType () {
const userTags = this.user.tags;
const tagsByType = {
@@ -514,6 +551,43 @@ export default {
this.tagsSnap[key].push({ id: uuid(), name: this.newTag });
this.newTag = null;
},
setActiveTag (index) {
this.activeTagIndex = index;
if (index === -1) {
const refArr = this.$refs.newTagInput;
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
} else {
const refArr = this.$refs[`tagInput-${index}`];
if (!refArr) {
this.textbox = null;
} else {
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
}
}
},
newTagEnterHandler (e, key) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
ac.makeSelection();
} else {
if (ac) ac.cancel();
this.addTag(e, key);
}
},
selectedTagAutocomplete (newText, newCaret) {
if (this.activeTagIndex === -1) {
this.newTag = newText;
} else {
this.tagsSnap.tags[this.activeTagIndex].name = newText;
}
this.$nextTick(() => {
if (this.textbox) {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
}
});
},
removeTag (index, key) {
const tagId = this.tagsSnap[key][index].id;
const indexInSelected = this.selectedTags.indexOf(tagId);
@@ -0,0 +1,92 @@
<template>
<div
class="selectable-card"
:class="{ selected }"
@click="$emit('click')"
>
<div
v-if="selected"
class="checkmark-corner"
>
<div
class="svg-icon check-icon"
v-html="icons.check"
></div>
</div>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.selectable-card {
position: relative;
background: $white;
border: 1px solid $gray-400;
border-radius: 8px;
padding: 16px;
cursor: pointer;
box-shadow: 0px 1px 2px 0px rgba(26, 24, 29, 0.08);
&:hover {
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
}
&.selected {
border: 2px solid $purple-300;
padding: 15px;
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
}
}
.checkmark-corner {
position: absolute;
top: 0;
left: 0;
width: 48px;
height: 48px;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
border-style: solid;
border-width: 48px 48px 0 0;
border-color: $purple-300 transparent transparent transparent;
border-radius: 6px 0 0 0;
}
.check-icon {
position: absolute;
top: 8px;
left: 8px;
width: 16px;
height: 16px;
color: $white;
}
}
</style>
<script>
import svgCheck from '@/assets/svg/check.svg?raw';
export default {
props: {
selected: {
type: Boolean,
default: false,
},
},
emits: ['click'],
data () {
return {
icons: Object.freeze({
check: svgCheck,
}),
};
},
};
</script>
@@ -398,14 +398,29 @@
:placeholder="$t('imageUrl')"
>
</div>
<div class="form-group">
<div class="form-group" style="position: relative;">
<label>{{ $t('about') }}</label>
<textarea
ref="blurbTextarea"
v-model="editingProfile.blurb"
class="form-control"
rows="5"
:placeholder="$t('displayBlurbPlaceholder')"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="editingProfile.blurb"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
<!-- include ../../shared/formatting-help-->
</div>
</div>
@@ -1001,6 +1016,8 @@ import mute from '@/assets/svg/mute.svg?raw';
import shadowMute from '@/assets/svg/shadow-mute.svg?raw';
import externalLinks from '../../mixins/externalLinks';
import { userCustomStateMixin } from '../../mixins/userState';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -1012,8 +1029,9 @@ export default {
MemberDetails,
profileStats,
toggleSwitch,
emojiAutoComplete,
},
mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
mixins: [externalLinks, userCustomStateMixin('userLoggedIn'), autoCompleteHelperMixin],
props: ['userId', 'startingPage'],
data () {
return {
@@ -1033,6 +1051,7 @@ export default {
mute,
shadowMute,
}),
textbox: null,
userIdToMessage: '',
editing: false,
editingProfile: {
@@ -1121,6 +1140,13 @@ export default {
userLoggedIn () {
this.loadUser();
},
editing (val) {
if (val) {
this.$nextTick(() => {
this.textbox = this.$refs.blurbTextarea;
});
}
},
},
mounted () {
this.loadUser();
@@ -1331,6 +1357,13 @@ export default {
this.$emit('toggled', this.isOpened);
},
selectedAutocomplete (newText, newCaret) {
this.editingProfile.blurb = newText;
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
reportPlayer () {
this.$root.$emit('habitica::report-profile', {
memberId: this.user._id,
@@ -1340,7 +1373,7 @@ export default {
},
openAdminPanel () {
this.$router.push(`/admin-panel/${this.hero._id}`);
this.$router.push(`/admin/panel/${this.hero._id}`);
},
},
};
@@ -43,7 +43,7 @@
<strong>{{ $t('equipment') }}:</strong>
<span :class="{ 'positive-stat': statsComputed.gearBonus[stat] !== 0 }">
{{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{
statsComputed.gearBonus[stat]
statsComputed.gearBonus[stat] + statsComputed.classBonus[stat]
}}
</span>
</li>
@@ -246,7 +246,9 @@
:class="{white: user.preferences.background}"
style="overflow:hidden"
>
<Sprite :image-name="'icon_background_' + user.preferences.background" />
<Sprite
v-if="user.preferences.background && user.preferences.background !== ''"
:image-name="'icon_background_' + user.preferences.background" />
</div>
<b-popover
v-if="label !== 'skip'
+1 -1
View File
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const { AMPLITUDE_KEY } = import.meta.env;
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
-18
View File
@@ -1,28 +1,10 @@
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
import moment from 'moment';
import i18n from '@/../../common/script/i18n';
function loadLocale (i18nData) {
// Load i18n strings
i18n.strings = i18nData.strings;
// Load Moment.js locale
const { language } = i18nData;
if (language && i18nData.momentLang && language.momentLangCode) {
// Make moment available under `window` so that the locale can be set
window.moment = moment;
// Execute the script and set the locale
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.type = 'text/javascript';
script.text = i18nData.momentLang;
head.appendChild(script);
moment.updateLocale(language.momentLangCode);
}
}
export default {
-2
View File
@@ -11,7 +11,6 @@ import {
NavbarPlugin,
CollapsePlugin,
} from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import { setUpLogging } from '@/libs/logging';
import router from './router/index';
@@ -44,7 +43,6 @@ Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin);
Vue.use(Fragment.Plugin);
setUpLogging();
const store = getStore();
+28 -14
View File
@@ -15,46 +15,60 @@ export const autoCompleteHelperMixin = {
};
},
methods: {
_getActiveAutocomplete () {
if (this.$refs.autocomplete && this.$refs.autocomplete.searchActive) {
return this.$refs.autocomplete;
}
if (this.$refs.emojiAutocomplete && this.$refs.emojiAutocomplete.searchActive) {
return this.$refs.emojiAutocomplete;
}
return null;
},
autoCompleteMixinHandleTab (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
if (e.shiftKey) {
this.$refs.autocomplete.selectPrevious();
ac.selectPrevious();
} else {
this.$refs.autocomplete.selectNext();
ac.selectNext();
}
}
},
autoCompleteMixinHandleEscape (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.cancel();
ac.cancel();
}
},
autoCompleteMixinSelectNextAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.selectNext();
ac.selectNext();
}
},
autoCompleteMixinSelectPreviousAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.selectPrevious();
ac.selectPrevious();
}
},
autoCompleteMixinSelectAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
if (this.$refs.autocomplete.selected !== null) {
const ac = this._getActiveAutocomplete();
if (ac) {
if (ac.selected !== null) {
e.preventDefault();
this.$refs.autocomplete.makeSelection();
ac.makeSelection();
} else {
// no autocomplete selected, newline instead
this.$refs.autocomplete.cancel();
ac.cancel();
}
}
},
+8 -50
View File
@@ -1,56 +1,14 @@
import includes from 'lodash/includes';
export default {
methods: {
foolPet (pet, prank) {
const SPECIAL_PETS = [
'Bear-Veteran',
'BearCub-Polar',
'Cactus-Veteran',
'Dragon-Hydra',
'Dragon-Veteran',
'Fox-Veteran',
'Gryphatrice-Jubilant',
'Gryphon-Gryphatrice',
'Gryphon-RoyalPurple',
'Hippogriff-Hopeful',
'Jackalope-RoyalPurple',
'JackOLantern-Base',
'JackOLantern-Ghost',
'JackOLantern-Glow',
'JackOLantern-RoyalPurple',
'Lion-Veteran',
'MagicalBee-Base',
'Mammoth-Base',
'MantisShrimp-Base',
'Orca-Base',
'Phoenix-Base',
'Tiger-Veteran',
'Turkey-Base',
'Turkey-Gilded',
'Wolf-Cerberus',
'Wolf-Veteran',
];
const BASE_PETS = [
'BearCub',
'Cactus',
'Dragon',
'FlyingPig',
'Fox',
'LionCub',
'PandaCub',
'TigerCub',
'Wolf',
];
if (!pet) return `Pet-TigerCub-${prank}`;
if (SPECIAL_PETS.indexOf(pet) !== -1) {
return `Pet-Dragon-${prank}`;
foolPet (pet, substitutions) {
if (!pet || pet === 'Pet-') return substitutions.noPet;
if (substitutions[pet]) return substitutions[pet];
for (const key in substitutions) {
if (pet.startsWith(key)) {
return substitutions[key];
}
}
const species = pet.slice(0, pet.indexOf('-'));
if (includes(BASE_PETS, species)) {
return `Pet-${species}-${prank}`;
}
return `Pet-BearCub-${prank}`;
return substitutions.default;
},
},
};
+1 -1
View File
@@ -8,7 +8,7 @@ import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
const { STRIPE_PUB_KEY } = import.meta.env;
let stripeInstance = null;
@@ -153,9 +153,23 @@
:placeholder="$t('needsTextPlaceholder')"
:maxlength="MAX_MESSAGE_LENGTH"
:class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keyup.ctrl.enter="sendPrivateMessage()"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="newMessage"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<div
class="sub-new-message-row d-flex"
@@ -540,6 +554,7 @@ h3 {
}
.new-message-row {
position: relative;
width: 100%;
padding-left: 1.5rem;
padding-top: 1.5rem;
@@ -676,6 +691,8 @@ import PmNewMessageStarted from './pm-new-message-started.vue';
import StartNewConversationInputHeader from './start-new-conversation-input-header.vue';
import positiveIcon from '@/assets/svg/positive.svg?raw';
import NotificationMixins from '@/mixins/notifications';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
// extract to a shared path
const CONVERSATIONS_PER_PAGE = 10;
@@ -700,13 +717,14 @@ export default defineComponent({
toggleSwitch,
userLink,
faceAvatar,
emojiAutoComplete,
},
filters: {
timeAgo (value) {
return moment(new Date(value)).fromNow();
},
},
mixins: [styleHelper, NotificationMixins],
mixins: [styleHelper, NotificationMixins, autoCompleteHelperMixin],
beforeRouteEnter (to, from, next) {
next(vm => {
const data = vm.$store.state.privateMessageOptions;
@@ -751,6 +769,7 @@ export default defineComponent({
/** @type {Record<string, PrivateMessages.PrivateMessageEntry[]>} */
messagesByConversation: {}, // cache {uuid: []}
textbox: null,
newMessage: '',
messages: [],
messagesLoading: false,
@@ -963,6 +982,15 @@ export default defineComponent({
}
},
},
watch: {
shouldShowInputPanel (val) {
if (val) {
this.$nextTick(() => {
this.textbox = this.$refs.textarea;
});
}
},
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('messages'),
@@ -1224,6 +1252,13 @@ export default defineComponent({
triggerStartNewConversationState () {
this.showStartNewConversationInput = true;
},
selectedAutocomplete (newText, newCaret) {
this.newMessage = newText;
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
async startConversationByUsername (targetUserName) {
// check if the target user exists in current conversations, select that conversation
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -90,7 +90,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment v-if="allowedToChangeClass">
<div class="d-content" v-if="allowedToChangeClass">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -71,7 +71,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -55,7 +55,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -77,7 +77,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -94,7 +94,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -78,7 +78,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -83,7 +83,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr>
<td class="settings-label">
{{ $t("showHeader") }}
@@ -26,7 +26,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -67,7 +67,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
@@ -39,7 +39,7 @@
</a>
</td>
</tr>
</fragment>
</div>
</template>
<script>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -66,7 +66,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -111,7 +111,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -56,7 +56,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -60,7 +60,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -54,7 +54,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -48,7 +48,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -76,7 +76,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
+4
View File
@@ -295,6 +295,10 @@ export default {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState.paymentType === 'groupPlan') {
this.$store.state.upgradingGroup = {};
this.$store.dispatch('guilds:getGroupPlans', true);
}
this.$root.$emit('habitica:payment-success', appState);
}
}
+47 -33
View File
@@ -11,56 +11,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
// NOTE: when adding a page make sure to implement the `common:setTitle` action
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
const Logout = () => import('@/components/auth/logout');
// Hall
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
const HallPage = () => import('@/components/hall/index');
const PatronsPage = () => import('@/components/hall/patrons');
const HeroesPage = () => import('@/components/hall/heroes');
// Admin Pages
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
const AdminContainerPage = () => import('@/components/admin/container');
const AdminPanelPage = () => import('@/components/admin/admin-panel');
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
const GroupAdminPage = () => import('@/components/admin/groups');
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
const BlockerPage = () => import('@/components/admin/blocker');
// Tasks
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
const UserTasks = () => import('@/components/tasks/user');
// Inventory
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
const InventoryContainer = () => import('@/components/inventory/index');
const ItemsPage = () => import('@/components/inventory/items/index');
const EquipmentPage = () => import('@/components/inventory/equipment/index');
const StablePage = () => import('@/components/inventory/stable/index');
// Guilds & Parties
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
const GroupPage = () => import('@/components/groups/group');
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
const LookingForParty = () => import('@/components/groups/lookingForParty');
// Group Plans
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
const GroupPlanIndex = () => import('@/components/group-plans/index');
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import('@/components/group-plans/billing');
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
// Challenges
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
const ChallengeIndex = () => import('@/components/challenges/index');
const MyChallenges = () => import('@/components/challenges/myChallenges');
const FindChallenges = () => import('@/components/challenges/findChallenges');
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
// Shops
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
const ShopsContainer = () => import('@/components/shops/index');
const MarketPage = () => import('@/components/shops/market/index');
const QuestsPage = () => import('@/components/shops/quests/index');
const CustomizationsPage = () => import('@/components/shops/customizations/index');
const SeasonalPage = () => import('@/components/shops/seasonal/index');
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
Vue.use(VueRouter);
@@ -85,6 +85,13 @@ const router = new VueRouter({
props: true,
},
{ name: 'profile', path: '/user/profile' },
{
name: 'avatar',
path: '/avatar',
children: [
{ name: 'backgrounds', path: 'backgrounds' },
],
},
{ name: 'stats', path: '/user/stats' },
{ name: 'achievements', path: '/user/achievements' },
{
@@ -410,6 +417,13 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('bv::hide::modal', 'profile');
}
if (to.name === 'backgrounds') {
store.state.avatarEditorOptions.editingUser = true;
store.state.avatarEditorOptions.startingPage = 'backgrounds';
router.app.$root.$emit('bv::show::modal', 'avatar-modal');
return null;
}
return next();
});
+2 -2
View File
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
const OverviewPage = () => import('@/components/static/overview');
const PressKitPage = () => import('@/components/static/pressKit');
const PrivacyPage = () => import('@/components/static/privacy');
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
const RegisterUsername = () => import('@/components/auth/registerUsername');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const TermsPage = () => import('@/components/static/terms');
+5 -1
View File
@@ -101,6 +101,7 @@ export async function appleAuth (store, params) {
id_token: params.idToken,
name: params.name,
username: params.username,
email: params.email,
},
});
@@ -109,7 +110,10 @@ export async function appleAuth (store, params) {
}
if (result.data.message && result.data.id_token) {
return { idToken: result.data.id_token };
return {
idToken: result.data.id_token,
email: result.data.email,
};
}
const user = result.data.data;
+1 -1
View File
@@ -122,7 +122,7 @@ export default defineConfig({
},
rollupOptions: {
output: {
experimentalMinChunkSize: 1000
experimentalMinChunkSize: 20000
}
}
},
+11 -4
View File
@@ -104,15 +104,15 @@
"achievementSkeletonCrewModalText": "Posbíral/a jsi všechna kostnatá zvířata!",
"achievementSkeletonCrewText": "Posbíral/a všechna kostnatá zvířata.",
"achievementLegendaryBestiaryModalText": "Posbíral/a jsi všechny mytické mazlíčky!",
"achievementLegendaryBestiaryText": "Posbíral/a jsi všechny základní barvy mytických mazlíčků: draka, létajícího prasete, gryfona, mořského hada a jednorožce!",
"achievementLegendaryBestiaryText": "Posbíral/a všechny barvy mytických mazlíčků: drak, létající prase, gryfon, mořský hady a jednorožec!",
"achievementLegendaryBestiary": "Legendární bestiář",
"achievementSeasonalSpecialist": "Sezónní specialista",
"achievementVioletsAreBlueText": "Získal/a všechny cukrově modré mazlíčky.",
"achievementVioletsAreBlue": "Fialky jsou Modré",
"achievementVioletsAreBlue": "Fialky jsou modré",
"achievementVioletsAreBlueModalText": "Posbíral/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
"achievementSeasonalSpecialistModalText": "Dokončl/a jsi všechny sezónní úkoly!",
"achievementDomesticatedModalText": "Sesbíral/a jsi všechna domácí zvířata!",
"achievementSeasonalSpecialistText": "Dokončil/a jsi všechny Jarní a Zimní sezónní úkoly: Honba za vajíčky, Pastičkář Santa, a najdi Cuba!",
"achievementSeasonalSpecialistText": "Splnil/a všechny jarní a zimní sezonní úkoly: Lov Vajec, Uvězněný Santa, a Najdi Mládě!",
"achievementWildBlueYonderText": "Ochočil/a všechny zvířata z Modré Cukrové Vaty.",
"achievementWildBlueYonderModalText": "Ochočil/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
"achievementDomesticatedText": "Vylíhl/a všechna standardní zbarvení domácích zvířat: Fretka, morče, kohout, létající prasátko, krysa, králík, kůň a kráva!",
@@ -157,5 +157,12 @@
"achievementBonelessBossModalText": "Získal/a jsi všechny bezobratlé mazlíčky!",
"achievementDuneBuddy": "Kámoš z dun",
"achievementDuneBuddyText": "Vylíhl/a jsi všechny, v poušti se vyskytující, mazlíčky: pásovce, kaktus, lišku, žábu, hada a pavouka!",
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!"
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!",
"achievementRodentRulerText": "Vylíhly se všechny standardní barvy hlodavců: morčata, krysy a veverky!",
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!",
"achievementRodentRuler": "Vládce hlodavců",
"achievementCats": "Pasák koček"
}
+4 -1
View File
@@ -735,5 +735,8 @@
"backgroundMaskMakersWorkshopText": "Maskářova dílna",
"backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.",
"backgroundCemeteryGateText": "Hřbitovní brána",
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány."
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány.",
"backgroundAutumnBridgeText": "Podzimní most",
"backgroundAutumnBridgeNotes": "Obdivuj krásu podzimního mostu.",
"backgroundInsideACrystalText": "Uvnitř krystalu."
}
+14 -3
View File
@@ -4,7 +4,7 @@
"brokenChaLink": "Nefunkční odkaz na výzvu",
"keepIt": "Ponechat",
"removeIt": "Odstranit",
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
"brokenChallenge": "Neplatný odkaz na výzvu",
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
"challenges": "Výzvy",
@@ -85,7 +85,7 @@
"summaryRequired": "Je požadováno shrnutí",
"summaryTooLong": "Shrnutí je příliš dlouhé",
"descriptionRequired": "Je požadován popis",
"locationRequired": "Je požadováno vybrat lokaci výzvy ('Přidat k')",
"locationRequired": "Je nutné vybrat umístění výzvy (Přidat do“)",
"categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
"viewProgressOf": "Zobrazit pokrok",
"viewProgress": "Zobrazit pokrok",
@@ -94,5 +94,16 @@
"selectParticipant": "Zvol účastníka",
"filters": "Filtry",
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
"yourReward": "Tvá odměna"
"yourReward": "Tvá odměna",
"brokenTaskDescription": "Tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš udělat?",
"brokenChallengeDescription": "Tento úkol byl součástí výzvy, ale výzva (nebo skupina) byla smazána. Co chceš udělat s osiřelými úkoly?",
"challengeCompletedDescription": "Vítězem je <%= user %>! Co chceš udělat s osiřelými úkoly?",
"messageChallengeFlagAlreadyReported": "Tuto výzvu jsi už nahlásil.",
"flaggedNotHidden": "Výzva byla nahlášena jednou, není skrytá",
"flaggedAndHidden": "Výzva byla nahlášena a je skrytá",
"resetFlagCount": "Resetovat počet nahlášení",
"deleteChallengeRefundDescription": "Pokud tuto výzvu smažeš, bude ti vrácena odměna v drahokamech a úkoly z výzvy zůstanou na nástěnkách úkolů účastníků.",
"messageChallengeFlagOfficial": "Oficiální výzvy nelze nahlásit.",
"brokenTask": "Nefunkční odkaz na výzvu",
"removeTasks": "Odstranit Úkoly"
}
+13 -7
View File
@@ -3,9 +3,9 @@
"iosFaqStillNeedHelp": "Jestli máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), použij formulář Ask a Question v sekci Nápověda na horní liště rozhraní. Jsme rádi když můžeme pomoct.",
"androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
"webFaqStillNeedHelp": "Pokud máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), přijď se zeptat do [Cechu „Habitica Help‟](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! Rádi ti pomůžeme.",
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví. \n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví.\n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
"webFaqAnswer26": "Pozitivní návyky (návyky, které chceš udržovat; měly by mít tlačítko plus)\n\n * Sněz vitamíny\n * Vyčisti si zuby\n * Hodina učení se\n\nNegativní návyky (návyky které chceš omezit nebo se jim zcela vyhnout; měly by mít tlačítko mínus)\n\n * Kouření\n * Bezmyšlenkovité scrollování\n * Kousání si nehtů\n\nOboustranné návyky (Návyky které mají jak pozitivní, tak negativní možnost; měly by mít tlačítko plus i mínus)\n\n * Pít vodu vs. Pít limonádu\n * Učit se vs. prokrastinovat\n\nNávrhy denních úkolů (úkoly, které chceš plnit pravidelně)\n * Umýt nádobí\n * Zalít kytky\n * 30 minut nějaké fyzické aktivity\n\nNávrhy úkolů do Úkolníčku (úkoly co chceš splnit jen jednou)\n\n * Objednat se k doktorovi\n * Zorganizovat obsah skříně\n * Dopsat esej",
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Stáj” and sjeď dolů, k sekci Stáj",
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Domácí zvířata a mounti” and sjeď dolů, k sekci Stáj",
"commonQuestions": "Časté otázky",
"faqQuestion25": "Jaké různé úkoly existují?",
"faqQuestion26": "Jaké úkoly mohu například vytvořit?",
@@ -13,19 +13,25 @@
"faqQuestion29": "Jak získám zpět Zdraví?",
"webFaqAnswer29": "Můžeš získat 15 bodů zdraví zakoupením Lektvaru zdraví ze sloupce Odměny za 25 zlaťáků. Navíc, pokud postoupíš do další úrovně, tak se ti všechno zdraví automaticky obnoví!",
"faqQuestion30": "Co se stane, když mi dojde zdraví?",
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit.",
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit. Můžete se znovu postavit plněním úkolů a opětovným zvyšováním úrovně.",
"faqQuestion31": "Proč jsem ztratil body, když jsem řešil úkol, který nebyl negativní?",
"webFaqAnswer31": "Když doděláš úkol a ztratíš zdraví i když bys správně neměl, narazil jsi na zpoždění, během kterého server synchronizoval změny na jiných platformách. Například, pokud použiješ zlaťáky, manu nebo ztratíš zkušenosti na aplikaci na mobilu a pak dokončíš akci na webově stránce, server jednoduše potvrzuje, že se všechno synchronizovalo.",
"faqQuestion32": "Kdy si mohu vybrat třídu?",
"webFaqAnswer32": "V Habitice existují čtyři třídy: Válečník, Mág, Zloděj a Léčitel. Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.",
"faqQuestion32": "Jak si mohu vybrat kurz?",
"webFaqAnswer32": "Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.\n\nPokud chcete změnit svou třídu po dosažení úrovně 10, můžete tak učinit pomocí Koule znovuzrození. Koule znovuzrození je k dispozici na trhu za 6 drahokamů na úrovni 50 nebo zdarma na úrovni 100.\n\nAlternativně můžete změnit třídu kdykoli v nastavení za 3 drahokamy. Tím se vaše úroveň nevynuluje jako v případě Koule znovuzrození, ale budete moci přerozdělit body dovedností, které jste nashromáždili při postupu na vyšší úroveň, tak aby odpovídaly vaší nové třídě.",
"faqQuestion33": "Co je to za modrou čáru s popisem Mana, která se objeví po dosažení 10. úrovně?",
"webFaqAnswer33": "Poté, co odemkneš třídní systém, tak odemkneš i schopnosti, jež ke svému použití vyžadují manu. Mana je učena tvou INT (inteligencí) a dá se měnit pomocí schopností a vybavení.",
"faqQuestion34": "Jaký typ jídla má rád můj mazlíček?",
"webFaqAnswer34": "Mazlíčci mají rádi jídla, která jim jdou barevně k srsti. Základní mazlíčci jsou výjimka, ale všichni základní mazlíčci mají rádi stejný předmět. Dole vidíš jídla, která mají specifičtí mazlíčci rádi:\n\n * Základní mazlíčci mají rádi maso\n * Bílí mazlíčci mají rádi mléko\n * Pouštní mazlíčci mají rádi brambory\n * Červení mazlíčci mají rádi jahody\n * Stínoví mazlíčci mají rádi čokoládu\n * Kostnatí mazlíčci mají rádi ryby\n * Zombie mazlíčci mají rádi hnijící maso\n * Cukrově růžoví mazlíčci mají rádi růžovou cukrovou vatu\n * Cukrově modří mazlíčci mají rádi modrou cukrovou vatu\n * Zlatí mazlíčci mají rádi med",
"faqQuestion35": "Nakrmil jsem svého mazlíčka a on zmizel! Co se stalo?",
"faqQuestion36": "Jak mohu změnit vzhled své postavy?",
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Upravit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Upravit postavu\"",
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Přizpůsobit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Přizpůsobit postavu\"",
"faqQuestion27": "Proč úkoly mění barvy?",
"webFaqAnswer27": "Barva úkolu je vizuální ukázkou hodnoty úkolu. Všechny úkoly začínají neutrálně žlutě, modrá je lepší a červená horší. Zde uvidíš jak typ úkolu určuje hodnotu úkolu:\n\nNávyky zmodrají nebo zčervenají podle toho, jestli klikneš na tlačítko plus nebo mínus. Pokud je nebudeš plnit, tak pozitivní a negativní úkoly oslabíš až na žlutou. Dvojité návyky mění barvy pouze na základě tvých zadání.\n\nDenní úkoly mění barvu podle toho, jak často jsou plněny a když se plní, stávají se modřejšími, nebo pokud jsou zanedbány, zčervenají.\n\nČím déle jsou úkoly v úkolníčku nesplněné, tím červenějšími se stávají.\n\nČím červenější úkol, tím víc zlaťáků a zkušeností získáš za jeho splnění, takže se vrhni i na ty nejdrsnější úkoly!",
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?"
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?",
"faqQuestion37": "Proč se mé vybavení neukazuje na mé postavě?",
"webFaqAnswer37": "Zkontrolujte, zda je zapnutá možnost Kostým. Pokud má váš avatar na sobě kostým, zobrazí se místo vaší bojové výstroje tato sada vybavení.\n\nZapnutí kostýmu v mobilních aplikacích:\n * V nabídce vyberte „Vybavení“ a najděte přepínač Kostým.\n\nZapnutí kostýmu na webových stránkách:\n * V inventáři vyberte „Vybavení“ a najděte přepínač Kostým v záložce Kostým v zásuvce Vybavení",
"faqQuestion38": "Proč nemohu zakoupit určité položky?",
"webFaqAnswer38": "Noví hráči Habitica mohou zakoupit pouze základní vybavení třídy válečník. Hráči musí nakupovat vybavení v pořadí, aby odemkli další kus.\n\nMnoho kusů vybavení je specifických pro danou třídu, což znamená, že hráč může zakoupit pouze vybavení patřící k jeho aktuální třídě.",
"faqQuestion39": "Kde mohu získat další vybavení?",
"faqQuestion40": "Co jsou gemy a jak je dostanu?"
}
+1 -1
View File
@@ -8,7 +8,7 @@
"rebirthOrb": "Použil Kouli Znovozrození, aby začal znova, po dosáhnutí úrovně <%= level %>.",
"rebirthOrb100": "Použil Kouli znovuzrození, aby začal odznovu po dosažení úrovně 100 nebo vyšší.",
"rebirthOrbNoLevel": "Použil Kouli Znovozrození, aby začal znova.",
"rebirthPop": "Obnoví tvou postavu a vrátí jí na 1. úroveň s povoláním Válečníka, zatímco ti zůstanou všechny úspěchy, celá sbírka a vybavení. Tvoje úkoly zůstanou i s historií, ale vrátí se na žlutou barvu. Tvé řady úspěchů se resetují, kromě úkolů patřících do aktivních výzev či do Skupiny. Tvé zlato, zkušenosti, mana a efekty všech schopností budou odstraněny. Toto vše nastane s okamžitou platností. Pro více informací se podívej na wiki stránku: <a href='https://habitica.fandom.com/wiki/Orb_of_Rebirth' target='_blank'>Orb znovuzrození</a>.",
"rebirthPop": "Obnoví tvou postavu a vrátí jí na 1. úroveň s povoláním Válečníka, zatímco ti zůstanou všechny úspěchy, celá sbírka a vybavení. Tvoje úkoly zůstanou i s historií, ale vrátí se na žlutou barvu. Tvé řady úspěchů se resetují, kromě úkolů patřících do aktivních výzev či do Skupiny. Tvé zlato, zkušenosti, mana a efekty všech schopností budou odstraněny. Toto vše nastane s okamžitou platností.",
"rebirthName": "Koule znovuzrození",
"rebirthComplete": "Byl jste znovuzrozen!",
"nextFreeRebirth": "<strong><%= days %> dni</strong> do <strong>bezplatného</strong> Koule znovuzrození"

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