Compare commits

..

322 Commits

Author SHA1 Message Date
SabreCat a5575b3593 4.245.2 2022-10-06 15:53:25 -05:00
SabreCat ace964f2b3 fix(subs): escape group demographics without group 2022-10-06 15:53:19 -05:00
SabreCat 12b045093a 4.245.1 2022-09-30 15:25:31 -05:00
SabreCat 8cd9536bdc Merge branch 'develop' into release 2022-09-30 15:25:27 -05:00
Weblate 08f0374b46 Translated using Weblate (Arabic)
Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Arabic)

Currently translated at 89.3% (334 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (208 of 208 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (208 of 208 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2695 of 2695 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2695 of 2695 strings)

Translated using Weblate (Spanish)

Currently translated at 95.0% (2562 of 2695 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (205 of 207 strings)

Translated using Weblate (Spanish)

Currently translated at 96.8% (125 of 129 strings)

Translated using Weblate (Spanish)

Currently translated at 99.1% (230 of 232 strings)

Translated using Weblate (Spanish)

Currently translated at 95.1% (370 of 389 strings)

Translated using Weblate (German)

Currently translated at 93.5% (364 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Polish)

Currently translated at 71.3% (517 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 70.4% (511 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 70.3% (510 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 69.7% (506 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Arabic)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Arabic)

Currently translated at 31.4% (57 of 181 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Arabic)

Currently translated at 37.0% (20 of 54 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (French)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (French)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (French)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2669 of 2691 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.4% (171 of 181 strings)

Translated using Weblate (Turkish)

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (Swedish)

Currently translated at 91.7% (166 of 181 strings)

Translated using Weblate (Serbian)

Currently translated at 90.6% (164 of 181 strings)

Translated using Weblate (Slovak)

Currently translated at 88.9% (161 of 181 strings)

Translated using Weblate (Romanian)

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (Portuguese)

Currently translated at 91.7% (166 of 181 strings)

Translated using Weblate (Polish)

Currently translated at 94.4% (171 of 181 strings)

Translated using Weblate (Dutch)

Currently translated at 97.2% (176 of 181 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.9% (161 of 181 strings)

Translated using Weblate (Hungarian)

Currently translated at 89.5% (162 of 181 strings)

Translated using Weblate (Hebrew)

Currently translated at 73.4% (133 of 181 strings)

Translated using Weblate (French)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (French)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Spanish)

Currently translated at 97.2% (176 of 181 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (English (Pirate) (en@pirate))

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (Danish)

Currently translated at 94.4% (171 of 181 strings)

Translated using Weblate (Czech)

Currently translated at 92.8% (168 of 181 strings)

Translated using Weblate (Bulgarian)

Currently translated at 91.7% (166 of 181 strings)

Translated using Weblate (French)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 92.2% (2482 of 2691 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.7% (613 of 2691 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (2672 of 2691 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (German)

Currently translated at 93.3% (363 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (2665 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (German)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (German)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (German)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 93.3% (363 of 389 strings)

Translated using Weblate (German)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.9% (2689 of 2691 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Faris Allahham <farislahham@gmail.com>
Co-authored-by: Felix Wittwer <spam@felixwittwer.de>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Juan Esteban Marín <juanmarin690@gmail.com>
Co-authored-by: Khsmty <me@taigasaito.org>
Co-authored-by: Lena Kubisa <lenorek.05.poczta@gmail.com>
Co-authored-by: Lio Zam <zerofux@web.de>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Nakonana <nanaki1989@web.de>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Nina Łapaj <ninapaj@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Remigiusz Haziak <haziakremigiusz@gmail.com>
Co-authored-by: Salman Mujeeb <kingleopard22@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Simon Fischer <simon.pascal.fischer@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/front/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/front/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/front/da/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en@pirate/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/he/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/es/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/de/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/es/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-09-30 22:22:14 +02:00
SabreCat 3582e233be fix(strings): remove duplicate "next" key 2022-09-30 15:09:53 -05:00
SabreCat 3974adcb65 chore(event): set Gem sale dates 2022-09-30 14:57:15 -05:00
SabreCat 57d3fea523 Merge branch 'group-tracking-modal' into release 2022-09-30 14:47:08 -05:00
SabreCat 173d7a178c Merge branch 'sabrecat/next-hourglass' into release 2022-09-30 14:46:27 -05:00
SabreCat 7db093d2bb Merge branch 'sabrecat/panel-subscription' into release 2022-09-30 14:46:10 -05:00
SabreCat 06d2ffb37d Merge branch 'sabrecat/gems-multi-event' into release 2022-09-30 14:45:36 -05:00
SabreCat 40997854dd fix(test): restore event list function 2022-09-30 14:32:21 -05:00
SabreCat 4c4d0be31f fix(analytics): include client tracking parameter 2022-09-29 15:07:21 -05:00
SabreCat b8cf1b895f 4.245.0 2022-09-28 15:34:17 -05:00
Natalie L a08ecbe044 chore(content): add October 2022 Mystery Items (#14254) 2022-09-28 15:04:47 -05:00
Natalie L f3771f4869 chore(content): add October 2022 Mystery Items (#14254) 2022-09-28 15:03:56 -05:00
SabreCat e0eed8238e fix(test): stub full event list 2022-09-23 16:42:15 -05:00
SabreCat 6baf08d461 fix(test): update expectations for new logic 2022-09-23 16:10:11 -05:00
SabreCat 535fddf92d feat(sale): add fine print 2022-09-21 16:28:15 -05:00
SabreCat ef97f301d9 Merge branch 'release' into sabrecat/gems-multi-event 2022-09-21 16:04:10 -05:00
SabreCat eea79ce1b6 Merge branch 'develop' into sabrecat/gems-multi-event 2022-09-21 16:03:57 -05:00
Weblate 191fee524c Merge branch 'origin/develop' into Weblate. 2022-09-21 22:23:36 +02:00
SabreCat 098f53bfa9 4.244.1 2022-09-21 15:02:39 -05:00
negue e9ee2d3fdd Transaction username mongodb mutation (#14231)
* migrate newest usernames in transactions

* fix lint

* change the parameters
2022-09-21 14:42:39 -05:00
SabreCat 4c988691cf fix(groups): show group task summary modal for site admins
also change "Watcher" to "Peeker" in fall healer gear
2022-09-21 14:39:46 -05:00
Weblate 3c8be16135 Translated using Weblate (Russian)
Currently translated at 99.8% (2686 of 2691 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 96.6% (175 of 181 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (2680 of 2691 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2685 of 2691 strings)

Translated using Weblate (Spanish)

Currently translated at 95.2% (2562 of 2691 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (232 of 232 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (2667 of 2691 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2691 of 2691 strings)

Translated using Weblate (Filipino)

Currently translated at 92.2% (167 of 181 strings)

Translated using Weblate (Filipino)

Currently translated at 94.5% (122 of 129 strings)

Translated using Weblate (Filipino)

Currently translated at 97.9% (96 of 98 strings)

Translated using Weblate (Filipino)

Currently translated at 40.9% (9 of 22 strings)

Translated using Weblate (Italian)

Currently translated at 98.9% (2664 of 2691 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Vince <vincemorel.vilan.889@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: weizhen lv <lvwzhen@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Subscriber
2022-09-21 19:12:26 +02:00
CuriousMagpie 9890e0079a fix: correct groupPlan.vue file 2022-09-21 12:02:35 -04:00
SabreCat 1530ab44e9 fix(gala): hide avatar customizations before start date 2022-09-20 16:57:39 -05:00
CuriousMagpie 586897fbfc Revert "update: remove another duplicate key from groups.json"
This reverts commit 014a4b653a.
2022-09-20 17:11:09 -04:00
SabreCat f75a6eb11d fix(hourglass): handle missing planId 2022-09-20 15:25:37 -05:00
SabreCat 155d6d5af6 Merge branch 'develop' into sabrecat/next-hourglass 2022-09-20 15:18:44 -05:00
SabreCat 004f1ee2dc Merge branch 'develop' into sabrecat/panel-subscription 2022-09-20 15:11:29 -05:00
CuriousMagpie c61bdaf563 Merge remote-tracking branch 'origin/group-tracking-modal' into group-tracking-modal 2022-09-20 15:36:58 -04:00
SabreCat 85e14bb100 Merge branch 'develop' into group-tracking-modal 2022-09-20 14:13:59 -05:00
CuriousMagpie 8e9b469d8d chore: merge develop 2022-09-20 13:11:43 -04:00
Weblate 6e5cac88fc Merge branch 'origin/develop' into Weblate. 2022-09-19 23:58:58 +02:00
Weblate 80acb70718 Translated using Weblate (German)
Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (German)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Belarusian)

Currently translated at 81.4% (615 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 97.2% (176 of 181 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (128 of 129 strings)

Translated using Weblate (Russian)

Currently translated at 99.9% (2662 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (180 of 181 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Czech)

Currently translated at 74.3% (1981 of 2663 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (754 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (German)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 93.3% (363 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.9% (612 of 2663 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.6% (352 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 45.8% (346 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (German)

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (388 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (2660 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (388 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (2660 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (57 of 58 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kővári <davson.kovari@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mattya 226 <worldworld1114@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/it/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/be/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translation: Habitica/Backgrounds
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2022-09-19 23:58:41 +02:00
SabreCat 24430861ce 4.244.0 2022-09-19 16:56:39 -05:00
SabreCat e60285e7d9 Merge branch 'develop' into release 2022-09-19 16:56:35 -05:00
Natalie L f030135c82 chore(content): add 2022 Fall Festival (#14244)
* chore(submodule): add August 2022 Mystery Items

* update(content): add 2022 Fall Festival content

* update(content): add 2022 Fall Festival content

* update(content): update event dates for release

* fix(lint): remove extra spaces
also correct typo in gear string keys

* feat(content): remaining descriptions

* fix(event): correct spell availability

* fix(test): work around first-match logic

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2022-09-19 16:55:53 -05:00
SabreCat 2c29310466 chore(privacy): official update note 2022-09-19 16:19:20 -05:00
CuriousMagpie 13eef6e4cf chore: merge develop 2022-09-19 15:10:11 -04:00
dependabot[bot] 13c0d12045 build(deps): bump rate-limiter-flexible from 2.3.7 to 2.3.10 (#14236)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 2.3.7 to 2.3.10.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/commits)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:33:30 -04:00
dependabot[bot] 6456984f57 build(deps): bump @babel/preset-env from 7.18.10 to 7.19.1 (#14232)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.18.10 to 7.19.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.19.1/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:32:25 -04:00
dependabot[bot] cfc1a12930 build(deps): bump jose from 2.0.5 to 2.0.6 (#14230)
Bumps [jose](https://github.com/panva/jose) from 2.0.5 to 2.0.6.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/v2.0.6/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v2.0.5...v2.0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:31:53 -04:00
dependabot[bot] 9ba4687478 build(deps): bump amplitude-js from 8.18.5 to 8.21.0 in /website/client (#14223)
Bumps [amplitude-js](https://github.com/amplitude/amplitude-javascript) from 8.18.5 to 8.21.0.
- [Release notes](https://github.com/amplitude/amplitude-javascript/releases)
- [Changelog](https://github.com/amplitude/Amplitude-JavaScript/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amplitude/amplitude-javascript/compare/v8.18.5...v8.21.0)

---
updated-dependencies:
- dependency-name: amplitude-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:30:19 -04:00
dependabot[bot] 6d987a9579 build(deps): bump apidoc from 0.52.0 to 0.53.0 (#14214)
Bumps [apidoc](https://github.com/apidoc/apidoc) from 0.52.0 to 0.53.0.
- [Release notes](https://github.com/apidoc/apidoc/releases)
- [Changelog](https://github.com/apidoc/apidoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apidoc/apidoc/compare/0.52.0...0.53.0)

---
updated-dependencies:
- dependency-name: apidoc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:27:35 -04:00
dependabot[bot] 4702479156 build(deps): bump vue and vue-template-compiler in /website/client (#14196)
Bumps [vue](https://github.com/vuejs/core) and [vue-template-compiler](https://github.com/vuejs/vue). These dependencies needed to be updated together.

Updates `vue` from 2.7.8 to 2.7.10
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits)

Updates `vue-template-compiler` from 2.7.8 to 2.7.10
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.7.8...v2.7.10)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: vue-template-compiler
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:26:17 -04:00
dependabot[bot] b384cd4eb8 build(deps): bump dompurify from 2.3.10 to 2.4.0 in /website/client (#14191)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.10 to 2.4.0.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.10...2.4.0)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:25:24 -04:00
dependabot[bot] d2bd7dc325 build(deps): bump jquery from 3.6.0 to 3.6.1 in /website/client (#14190)
Bumps [jquery](https://github.com/jquery/jquery) from 3.6.0 to 3.6.1.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.6.0...3.6.1)

---
updated-dependencies:
- dependency-name: jquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-19 14:24:01 -04:00
CuriousMagpie 719fab8d4b chore: merge develop in 2022-09-16 16:12:34 -04:00
Phillip Thelen 24841346dc Purge Facebook (#13696)
* Don't sign in user when trying to connect a social account that was already created

* Log social users into matching local auth accounts

If the social account has an email that already exists as a local user, instead of creating a new account log them into their account and add the social auth to the account

* If possible set local authentication email for social users

* Allow password reset emails to be sent to social login users

* lint fixes

* Fix issues and tests

* fix tests

* Fix lint error.

* purge Facebook.

Only keep it in some select places to allow for some compatablilty.

* Fix error

* fix error

* Let settings handle it when you don't have a password set but an email

* fix error

* Fix boolean logic

* fix json conversion

* .

* fix password reset for old social accounts

* Don't sign in user when trying to connect a social account that was already created

* Log social users into matching local auth accounts

If the social account has an email that already exists as a local user, instead of creating a new account log them into their account and add the social auth to the account

* If possible set local authentication email for social users

* Allow password reset emails to be sent to social login users

* lint fixes

* Fix issues and tests

* fix tests

* Fix lint error.

* purge Facebook.

Only keep it in some select places to allow for some compatablilty.

* Fix error

* fix error

* Let settings handle it when you don't have a password set but an email

* fix error

* Fix boolean logic

* fix json conversion

* fix password reset for old social accounts

* Revert "lint fixes"

This reverts commit c244b1651c.

# Conflicts:
#	website/client/src/components/auth/registerLoginReset.vue
#	website/client/src/components/static/contact.vue

* Revert "fix password reset for old social accounts"

This reverts commit 7e0069a80f.

* fix duplicate code

* chore(misc): remove irrelevant changes

* chore(privacy): update policy page with note about FB

Co-authored-by: SabreCat <sabe@habitica.com>
2022-09-15 18:22:52 -05:00
SabreCat 4c34c68d78 fix(test): stub newly relevant function 2022-09-15 12:20:11 -05:00
SabreCat 9a8d1854b9 fix(promo): handle Gems sale defined outside of single top event 2022-09-15 11:40:48 -05:00
SabreCat 10f5011781 chore(subproject): update habitica-images SHA 2022-09-15 11:35:27 -05:00
Natalie L 9a896470d5 Site Footer Update (#14134)
* first pass

* second pass

* third pass

* fourth pass, not a lot of progress

* fifth pass, slowly gettin there

* breakpoint nonsense

* flexbox and breakpoint nonsense

* svg updates, css updates

* whitespace

* chore: merge upstream/develop

* style(breakpoints): added responsive breakpoints, social column alignment tweaks

* style(breakpoints): add xs-specific selectors and classes

* style: mobile footer

* style: mobile footer

* style: static landing page footer

* style: small tweaks to social layout

* fix(translations): update website/common/locales/de/gear.json to develop

* update(style): small updates

* update(style): cleaning up code

* update(style): cleaning up code

* update(style): mobile footer updates

* update(style): complete mobile footer & clean up code

* update(style): add tablet breakpoint and remove rogue terms link
2022-09-14 16:35:24 -05:00
Natalie L 6b0b393e32 fix: Group Plans string duplication & style adjustment (#14148)
* fix(string): questVice1Notes html changed to a mobile-device friendly format

* fix(strings): updated limited.json with "dateEnd" & "monthYYYY" months & put in chronological order

* fix(string): remove extra word from headSpecialSummer2022WarriorNotes

* fix(string): corrected armorSpecialSummer2022MageNotes

* fix: remove duplicated string and adjust upgrade button style

* fix(style): set border radii to 8px on upgrading-group id

* fix(payments): remove duplicate entry from another modal

Co-authored-by: SabreCat <sabe@habitica.com>
2022-09-14 16:33:31 -05:00
CuriousMagpie 014a4b653a update: remove another duplicate key from groups.json 2022-09-14 17:24:29 -04:00
CuriousMagpie 37e5d6b40a update: remove duplicate key from groups.json 2022-09-14 17:06:33 -04:00
SabreCat fb780c9a2d fix(admin): reactivity with subscriptions 2022-09-13 15:52:03 -05:00
Weblate 5f440f1bfa Merge branch 'origin/develop' into Weblate. 2022-09-13 17:11:21 +02:00
SabreCat d4f9555f11 4.243.1 2022-09-13 10:11:17 -05:00
Weblate 203d97423a Translated using Weblate (Russian)
Currently translated at 97.9% (381 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Danish)

Currently translated at 94.0% (127 of 135 strings)

Translated using Weblate (Danish)

Currently translated at 89.3% (84 of 94 strings)

Translated using Weblate (Danish)

Currently translated at 97.2% (175 of 180 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Danish)

Currently translated at 86.0% (185 of 215 strings)

Translated using Weblate (Danish)

Currently translated at 93.3% (126 of 135 strings)

Translated using Weblate (Danish)

Currently translated at 71.0% (147 of 207 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Danish)

Currently translated at 88.2% (83 of 94 strings)

Translated using Weblate (Danish)

Currently translated at 99.0% (110 of 111 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Danish)

Currently translated at 99.1% (226 of 228 strings)

Translated using Weblate (Danish)

Currently translated at 78.9% (307 of 389 strings)

Translated using Weblate (Danish)

Currently translated at 68.6% (1829 of 2663 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Danish)

Currently translated at 96.6% (174 of 180 strings)

Translated using Weblate (Danish)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Danish)

Currently translated at 81.1% (613 of 755 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Danish)

Currently translated at 67.8% (492 of 725 strings)

Translated using Weblate (Danish)

Currently translated at 85.5% (184 of 215 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.9% (611 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (379 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.7% (330 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 42.7% (323 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.8% (316 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.7% (315 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (206 of 207 strings)

Translated using Weblate (Russian)

Currently translated at 95.6% (372 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Danish)

Currently translated at 98.6% (225 of 228 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (German)

Currently translated at 92.5% (360 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Asta Jensen <asta.raae@live.dk>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: KanI <twinklingnerd@gmail.com>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/da/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/da/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/da/
Translate-URL: https://translate.habitica.com/projects/habitica/content/da/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/da/
Translate-URL: https://translate.habitica.com/projects/habitica/death/da/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/da/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/da/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/da/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/da/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/da/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/da/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/da/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/da/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/da/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/da/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/da/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/da/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/da/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/da/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/da/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-09-13 17:11:08 +02:00
Natalie L 0f4711c358 chore(content): add 2022-09 Pet Quest bundle (#14218)
* chore(content): farmFriends quest bundle

* chore(content): 2022-09 Pet Quest Bundle

* fix(date): fix start date
2022-09-13 09:12:24 -05:00
SabreCat 1a86943711 fix(subscriptions): better next-hourglass logic 2022-09-12 15:17:39 -05:00
CuriousMagpie 21185b689c update: add 'type' to amazonModal.vue, removed extraneous analytics.js code 2022-09-12 11:42:39 -04:00
CuriousMagpie 5a85e0730c update: analytics debugging 2022-09-09 16:56:19 -04:00
CuriousMagpie 771558e1fd update: analytics debugging 2022-09-09 16:27:25 -04:00
CuriousMagpie e6f903fd2e update: more analytics work 2022-09-09 15:37:08 -04:00
CuriousMagpie 5c13bf1980 update: add analytics to create and success modals 2022-09-08 17:34:36 -04:00
CuriousMagpie 36a4ec69d5 update: success modal (testing something) 2022-09-08 15:53:26 -04:00
CuriousMagpie c6ba1d8402 update: success modal 2022-09-08 12:55:53 -04:00
SabreCat 0081bad831 feat(admin): track hourglasses as transactions 2022-09-06 16:15:04 -05:00
CuriousMagpie abdb6244d3 updates: add comments to success modal to flag issues 2022-09-06 17:06:19 -04:00
SabreCat 90f1977a49 fix(admin): sometimes 0 is correct 2022-09-06 15:09:40 -05:00
SabreCat bc33e4349d Merge branch 'develop' into sabrecat/panel-subscription 2022-09-06 14:19:46 -05:00
CuriousMagpie 61f3d8d61c Merge branch 'develop' into group-tracking-modal 2022-09-06 15:11:03 -04:00
SabreCat 38bf0b3721 4.243.0 2022-09-06 12:35:18 -05:00
SabreCat 88c8b545f4 Merge branch 'develop' into release 2022-09-06 12:35:14 -05:00
Weblate 184ee7262e Translated using Weblate (Japanese)
Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.3% (312 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.3% (158 of 207 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.8% (157 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (French)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (125 of 128 strings)

Translated using Weblate (French)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2653 of 2659 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.1% (311 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (German)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Danish)

Currently translated at 80.5% (608 of 755 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Danish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Chinese (Hong Kong))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.2% (133 of 207 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 79.3% (570 of 718 strings)

Translated using Weblate (German)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2652 of 2659 strings)

Translated using Weblate (German)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.2% (359 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (206 of 207 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (127 of 128 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (376 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (376 of 389 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (128 of 128 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Asta Jensen <asta.raae@live.dk>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: JoanZeppeli <x17501668978@163.com>
Co-authored-by: Khsmty <me@taigasaito.org>
Co-authored-by: Mara S. (Dolichotis) <marascherzer@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara de Nicolas <saradenicolas12@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Simon Fischer <simon.pascal.fischer@gmail.com>
Co-authored-by: Tiffany Tai <mrbroccoli128@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zero <leedambak@gmail.com>
Co-authored-by: neko kyuri <Nekorin0621@gmail.com>
Co-authored-by: weizhen lv <lvwzhen@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/da/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hant_HK/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/da/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/it/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/de/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/da/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/de/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2022-09-06 19:34:47 +02:00
Natalie L 6df4ce251c chore(content): add September 2022 Backgrounds and Enchanted Armoire Items (#14200)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add September 2022 Backgrounds and Enchanted Armoire Items

* fix(typo): periods so people don't grumble

* fix(typo): consistent caps

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-09-06 12:31:55 -05:00
CuriousMagpie 796d752974 update: success modal 2022-09-02 16:55:28 -04:00
SabreCat 008314676d fix(admin): reactivity on next hourglass 2022-09-01 17:16:51 -05:00
SabreCat e383614107 4.242.1 2022-09-01 14:47:20 -05:00
Natalie L 2b21410abd fix(quest shop): correct forest friends end date (#14202) 2022-09-01 14:45:49 -05:00
SabreCat f364b3c06f feat(admin): consecutive months editable field and automatic calcs 2022-08-31 16:09:20 -05:00
SabreCat ae23ac12ff feat(admin): interactive subscription section fields 2022-08-30 16:56:43 -05:00
CuriousMagpie e2bb7fda60 update: success modal 2022-08-30 17:43:36 -04:00
SabreCat 09d6dae75c feat(admin): provide reset cron button 2022-08-30 15:35:50 -05:00
SabreCat bc5813fd10 Merge branch 'develop' into sabrecat/panel-subscription 2022-08-30 14:54:40 -05:00
CuriousMagpie 4464464c51 Merge branch 'develop' into group-tracking-modal 2022-08-30 15:44:09 -04:00
Weblate 56f956be5a Merge branch 'origin/develop' into Weblate. 2022-08-30 21:25:56 +02:00
Weblate 2b44d32b1c Translated using Weblate (Ukrainian)
Currently translated at 39.3% (81 of 206 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.1% (311 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 94.8% (369 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.9% (118 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 78.6% (565 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.4% (174 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 64.0% (132 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (English (Pirate) (en@pirate))

Currently translated at 88.5% (124 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 43.8% (79 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 41.1% (74 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (30 of 58 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Korean)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 63.5% (131 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Czech)

Currently translated at 74.4% (1977 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.7% (100 of 127 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (French)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.5% (360 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Czech)

Currently translated at 74.2% (1972 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 98.9% (93 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 73.8% (1962 of 2655 strings)

Translated using Weblate (Romanian)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 72.8% (1934 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Romanian)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Romanian)

Currently translated at 90.0% (126 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Italian)

Currently translated at 99.2% (386 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (354 of 367 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Filipino)

Currently translated at 81.3% (109 of 134 strings)

Translated using Weblate (Filipino)

Currently translated at 95.0% (133 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 36.3% (8 of 22 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.8% (214 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.4% (204 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (2642 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (2640 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 50.0% (11 of 22 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.6% (602 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (2636 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.6% (786 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Filipino)

Currently translated at 72.9% (551 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (135 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 93.4% (200 of 214 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.8% (1163 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.8% (198 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (2628 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.0% (108 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 57.8% (1535 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2623 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.4% (197 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.7% (1667 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 72.1% (101 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.1% (56 of 206 strings)

Translated using Weblate (French)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.4% (1951 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 40.9% (309 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Korean)

Currently translated at 92.3% (339 of 367 strings)

Translated using Weblate (Filipino)

Currently translated at 86.3% (620 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 74.7% (564 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 74.2% (104 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.6% (96 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.5% (2005 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 75.3% (569 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 78.5% (110 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Dutch)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.9% (490 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.3% (554 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.8% (148 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.9% (180 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.7% (2010 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.1% (173 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.5% (44 of 56 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.6% (594 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (114 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 90.0% (162 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (206 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (108 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 86.1% (316 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1783 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 72.2% (39 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 91.4% (43 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 78.6% (169 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 92.8% (52 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (107 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.8% (119 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (4 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 12.1% (17 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 83.6% (51 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 77.2% (17 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 85.8% (315 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1782 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (39 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 70.8% (90 of 127 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2589 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Filipino)

Currently translated at 84.6% (94 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 79.2% (111 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 94.1% (351 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 86.4% (96 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 82.1% (115 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 82.8% (116 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 83.5% (117 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.9% (173 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (903 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2588 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (French)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2587 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.4% (120 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.6% (2647 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.9% (113 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 79.5% (101 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2586 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2583 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2581 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 87.3% (180 of 206 strings)

Translated using Weblate (French)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.0% (2576 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 86.8% (179 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Korean)

Currently translated at 74.8% (95 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2632 of 2655 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.7% (702 of 718 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (French)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.7% (2568 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 82.4% (113 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 42.3% (1125 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 38.3% (69 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 90.1% (193 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 96.5% (2563 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (214 of 215 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (2559 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (107 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 89.9% (116 of 129 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 76.6% (105 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 64.4% (1711 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 83.3% (45 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (44 of 54 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 44.6% (25 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 95.6% (178 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1784 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (93 of 111 strings)

Translated using Weblate (Galician)

Currently translated at 21.1% (38 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (179 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 89.5% (120 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 83.1% (628 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 13.8% (19 of 137 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 87.1% (320 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.3% (1789 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 89.8% (335 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 74.0% (94 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 76.7% (165 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 3.8% (7 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 96.4% (54 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 57.1% (410 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 58.7% (121 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 91.1% (195 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 73.2% (93 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 74.4% (160 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (754 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 75.9% (104 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Co-authored-by: Adriana Alupei <a.ady96@yahoo.com>
Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Anton de Regt <antonderegt@pm.me>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kővári <davson.kovari@gmail.com>
Co-authored-by: Felix Wittwer <spam@felixwittwer.de>
Co-authored-by: Forst Wolf <forestwolf@spam.care>
Co-authored-by: Forstwolf <forestwolf@spam.care>
Co-authored-by: Goggle K <afc731@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Hyun Yeol Kim <hyunyeolkim@gmail.com>
Co-authored-by: JMFO16 <fournier.olivera.jm@gmail.com>
Co-authored-by: Kedr <sergeysamori.ua@gmail.com>
Co-authored-by: Leslie Munguía <moongeeuh@gmail.com>
Co-authored-by: Linda Li <wli62442@gmail.com>
Co-authored-by: Martim Pinto Paiva <pintopaivam@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Richard Gould <rgould@u2622.ca>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sara López <sarayupy@gmail.com>
Co-authored-by: Sara de Nicolas <saradenicolas12@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Tran Lam Van Khoa <lamvankhoat1@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Ventus Meigo <at.fbfzd@gmail.com>
Co-authored-by: Vince <vincemorel.vilan.889@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Zero <leedambak@gmail.com>
Co-authored-by: datschka <datschka@gmx.at>
Co-authored-by: hekin zhou <1916360372@qq.com>
Co-authored-by: jiangshanghan <jsh1215@hash.fyi>
Co-authored-by: kat o(`ω´ )o <memesarerealkool@gmail.com>
Co-authored-by: neko kyuri <Nekorin0621@gmail.com>
Co-authored-by: parkbird <kgh9812@naver.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en@pirate/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/death/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/it/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
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/inventory/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/it/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/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
2022-08-30 21:25:33 +02:00
SabreCat 11347e5679 Merge branch 'release' into develop 2022-08-30 14:25:25 -05:00
SabreCat a04479e689 4.242.0 2022-08-30 14:21:26 -05:00
SabreCat 4ce4e55e80 chore(git): update subproject commit 2022-08-30 14:20:50 -05:00
Natalie L 755f51b674 chore(content): add September 2022 Mystery Items (#14199)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add September 2022 Mystery Items

* fix(typo): verb form

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-30 14:06:06 -05:00
SabreCat 6c1b21117f fix(teams): Stealth calc and small screens 2022-08-29 17:09:06 -05:00
SabreCat 14441701c9 Merge branch 'release' into develop 2022-08-29 15:51:45 -05:00
CuriousMagpie a3e6aff330 update: more work on success modal 2022-08-29 16:12:53 -04:00
CuriousMagpie a523d0b894 Merge branch 'develop' into group-tracking-modal 2022-08-29 13:33:15 -04:00
CuriousMagpie 8a809c3828 update: work on success modal, update to payment mixin to calculate monthly billing date 2022-08-26 17:58:51 -04:00
Anton de Regt ccb821fd6f Fix redirect after register issue (#14182) 2022-08-26 15:56:58 -05:00
Natalie L 3664a1ebb1 fix(tavern): update Pause Damage Description, and Staff list (#14174) 2022-08-26 15:51:21 -05:00
Natalie L 509cb00374 fix(api): add API version (#14177) 2022-08-26 15:46:43 -05:00
theneelshah bc4770577a Fix prop change handler for guild challenges. (#14169)
Fix props' change handler which is called when guild is changed from
notification.

Tests:

+ Guild challenges updated successfully when guild is changed from
notification center.

Co-authored-by: neel <neel@helpshift.com>
2022-08-26 15:45:32 -05:00
Jason Mishi Carvalho f158852be5 Grey out skill when insufficient mana fixes #13286 (#14100)
* disable spell if user doesn't have enough mana

* differenciate insufficient mana and disabled spell

* linting

* reduce opacity, no hover state when insufficient mana

* display that lvl insufficient in spell tooltip

* change spell text color when spell has no effect

change spell-text color to blue-500 when spell has no effect
2022-08-26 15:22:04 -05:00
CuriousMagpie 40f433b099 update: work on success modal 2022-08-25 15:47:24 -04:00
CuriousMagpie 9a1266677a Merge remote-tracking branch 'upstream/develop' into group-tracking-modal 2022-08-25 13:20:54 -04:00
SabreCat ee0f6fd78f fix(test): same thing for v3 2022-08-25 10:35:49 -05:00
SabreCat 3284611bbf fix(test): adjust expectation for exploit fix 2022-08-25 10:22:16 -05:00
SabreCat 986d38af69 4.241.4 2022-08-25 09:24:17 -05:00
SabreCat 7129639bbf fix(misc): correct one groups issue and two others 2022-08-25 09:24:08 -05:00
SabreCat 6aabf7b19a 4.241.3 2022-08-24 14:18:20 -05:00
SabreCat a5d9448af1 fix(cron): don't process group tasks during user cron 2022-08-24 14:18:07 -05:00
SabreCat 6e19a0ef2e Merge branch 'release' into develop 2022-08-24 11:06:11 -05:00
SabreCat bc8b1884b7 4.241.2 2022-08-24 11:05:36 -05:00
SabreCat 1aae9638ec fix(tasks): address regressions from group plan rollout 2022-08-24 11:05:19 -05:00
dependabot[bot] e6b0c1e488 build(deps): bump @babel/core from 7.18.10 to 7.18.13 (#14184)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.10 to 7.18.13.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.13/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-23 19:57:59 -04:00
dependabot[bot] d5bbc9599c build(deps): bump core-js from 3.23.5 to 3.24.1 in /website/client (#14154)
Bumps [core-js](https://github.com/zloirock/core-js) from 3.23.5 to 3.24.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/compare/v3.23.5...v3.24.1)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-23 19:49:41 -04:00
SabreCat a2f191089c 4.241.1 2022-08-23 14:38:09 -05:00
SabreCat 75c8486b1a fix(checklists): allow scoring own items 2022-08-23 14:38:05 -05:00
Sabe Jones 20854057ad fix(migration): handle orphaned assignments 2022-08-23 19:18:01 +00:00
SabreCat ae3f064197 4.241.0 2022-08-23 12:49:25 -05:00
SabreCat 67ee0b72d3 fix(tasks): don't show reset counter control on group tasks 2022-08-23 12:40:32 -05:00
SabreCat aebf13810f fix(tasks): remove spurious uncheck notification 2022-08-23 11:33:06 -05:00
SabreCat 971891dd6b fix(tasks): no really, address not-found error 2022-08-23 10:48:21 -05:00
SabreCat 395b8db932 fix(tasks): fix unassigned error case 2022-08-23 09:54:44 -05:00
SabreCat da5c3f9602 fix(tasks): address not-found error on open uncheck 2022-08-23 09:36:23 -05:00
SabreCat 4c85b933cb fix(tests): correct one last v3 test and wrap v4 2022-08-22 21:59:51 -05:00
SabreCat 82abdaa0c4 WIP(tests): finish cleaning up v3 integrations 2022-08-22 21:47:55 -05:00
SabreCat 02c50b6126 WIP(tests): fix various assign requests and needs-work flow 2022-08-22 20:45:22 -05:00
SabreCat 3ab88bbb3f fix(tests): short circuit getter, adjust expectations 2022-08-22 18:49:56 -05:00
SabreCat 5251598369 fix(cron): fix score down breaking during middleware 2022-08-22 16:38:51 -05:00
SabreCat 149da578fd fix(teams): fix fix fix
Removed testing banner
Fixed a JS console error when assigning a user to a previously open task
Fixed a potential abuse where user might be able to score someone else's 
task via API call
Fixed an issue where finding tasks by alias could return tasks belonging 
to other users
Fixed an issue that was appending the user's party ID to their list of 
Guilds
Fixed an issue where group tasks were not receiving the default tag 
needed for filtering them on user's personal list
2022-08-22 16:16:23 -05:00
SabreCat 35d963a397 fix(teams): tweak FAQ and fix sync test 2022-08-22 14:43:21 -05:00
SabreCat cccd8c3b1b Revert "fix(tests): catch non-array parameter"
This reverts commit 595c131398.
2022-08-22 14:17:06 -05:00
SabreCat 631d7111a5 fix(teams): send @username in notifications 2022-08-22 11:39:53 -05:00
SabreCat 89c07529ea feat(teams): add FAQ entry
Also a few client side fixes
2022-08-22 11:27:08 -05:00
SabreCat 595c131398 fix(tests): catch non-array parameter 2022-08-19 17:13:48 -05:00
SabreCat f063b9e81c fix(tests): sanity and common 2022-08-19 16:10:18 -05:00
SabreCat 49a20218a5 fix(onboarding): improve modal launching and clicky behavior 2022-08-19 14:09:41 -05:00
SabreCat 95714599f0 feat(onboarding): welcome modal 2022-08-19 13:04:48 -05:00
SabreCat 5893312d75 Merge branch 'develop' into sabrecat/teams-rebase 2022-08-17 14:28:43 -05:00
CuriousMagpie d70dd2e6dd update: add analytics event upon group creation/upgrade (temp code & console.logs commented out) 2022-08-15 17:13:51 -04:00
SabreCat 71fa4d6cb7 Merge branch 'release' into develop 2022-08-15 15:36:18 -05:00
SabreCat 922b2e985a 4.240.0 2022-08-15 15:29:12 -05:00
SabreCat b82239811c chore(content): add items to featured 2022-08-15 15:28:47 -05:00
Natalie L f0b5637e9e chore(content): add Porcelain Magic Hatching Potion (#14168)
* chore(submodule): add August 2022 Mystery Items

* chore: update habitica-images

* chore(content): add Porcelain Magic Hatching Potion

* chore(content): update moonglow potion availability

* fix(events): no gap between events

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-15 15:14:23 -05:00
CuriousMagpie f078d19e4b Merge branch 'develop' into group-tracking-modal 2022-08-15 13:49:35 -04:00
dependabot[bot] 2c93b3e2e3 build(deps): bump @babel/preset-env from 7.18.6 to 7.18.10 (#14158)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.18.6 to 7.18.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 13:31:37 -04:00
dependabot[bot] 72a9417de9 build(deps): bump @babel/core from 7.18.6 to 7.18.10 (#14157)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.18.6 to 7.18.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.10/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 13:06:55 -04:00
CuriousMagpie e08d0f4016 update: style changes, add upgraded group demographics to success modal, add description counter 2022-08-12 16:44:58 -04:00
CuriousMagpie 0435e3537a update: add temp button & function for triggering success modal; add comments 2022-08-11 16:39:49 -04:00
SabreCat 0aadee550e fix(admin): add data value for expand state 2022-08-11 14:16:27 -05:00
CuriousMagpie f487837b4b Merge branch 'develop' into group-tracking-modal 2022-08-11 15:02:40 -04:00
SabreCat b593db2150 feat(admin): show subscription data 2022-08-10 16:20:32 -05:00
CuriousMagpie a07c2e6268 update: dropdown and delete duplicated file 2022-08-10 16:48:09 -04:00
SabreCat e3c552dd54 WIP(dropdown): add placeholder text 2022-08-10 16:02:56 -04:00
CuriousMagpie 8d1f7e77ed update: styling and dropdown 2022-08-10 14:00:46 -04:00
SabreCat 1701fc702b Merge branch 'develop' into sabrecat/teams-rebase 2022-08-09 11:55:46 -05:00
SabreCat 16dc6a1b4c Merge branch 'release' into develop 2022-08-09 11:54:45 -05:00
SabreCat d3a91aab72 4.239.0 2022-08-09 11:52:26 -05:00
Natalie L 0528ee1761 fix(dates): update end dates for quest bundle (#14167)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add Woodland Wizard achievement

* chore(content): add Forest Friends quest bundle

* fix(typo): whitespace

* fix(dates): update end date for quest bundle

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-09 11:50:05 -05:00
CuriousMagpie cfd601e7bf update: styling and some function work 2022-08-08 17:16:41 -04:00
SabreCat 1b4d670b0a fix(tasks): styles and wordings 2022-08-08 15:50:37 -05:00
CuriousMagpie 5d81c63897 update: upgrade groups sign up page (slightly) 2022-08-08 16:36:14 -04:00
SabreCat 3654e01fee Merge branch 'develop' into sabrecat/teams-rebase 2022-08-08 12:13:29 -05:00
SabreCat fdbeda19e2 fix(tasks): icons and select lists 2022-08-08 12:13:15 -05:00
dependabot[bot] db723d79a4 build(deps): bump dompurify from 2.3.8 to 2.3.10 in /website/client (#14132)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.8 to 2.3.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.8...2.3.10)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:22:23 -04:00
dependabot[bot] d5d1bfbd99 build(deps): bump terser from 4.6.7 to 4.8.1 in /website/client (#14133)
Bumps [terser](https://github.com/terser/terser) from 4.6.7 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:21:48 -04:00
dependabot[bot] d06f4f4e1e build(deps-dev): bump @babel/plugin-proposal-optional-chaining (#14135)
Bumps [@babel/plugin-proposal-optional-chaining](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-optional-chaining) from 7.18.6 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/packages/babel-plugin-proposal-optional-chaining)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-optional-chaining"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:21:20 -04:00
dependabot[bot] 82c4260fca build(deps-dev): bump run-rs from 0.7.6 to 0.7.7 (#14138)
Bumps [run-rs](https://github.com/vkarpov15/run-rs) from 0.7.6 to 0.7.7.
- [Release notes](https://github.com/vkarpov15/run-rs/releases)
- [Changelog](https://github.com/vkarpov15/run-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vkarpov15/run-rs/compare/0.7.6...0.7.7)

---
updated-dependencies:
- dependency-name: run-rs
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:57 -04:00
dependabot[bot] 0f3a26a490 build(deps): bump @babel/register from 7.18.6 to 7.18.9 (#14140)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.18.6 to 7.18.9.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.18.9/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:40 -04:00
dependabot[bot] 9c2963e557 build(deps): bump vue and vue-template-compiler in /website/client (#14143)
Bumps [vue](https://github.com/vuejs/core) and [vue-template-compiler](https://github.com/vuejs/vue). These dependencies needed to be updated together.

Updates `vue` from 2.6.14 to 2.7.8
- [Release notes](https://github.com/vuejs/core/releases)
- [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/core/commits)

Updates `vue-template-compiler` from 2.6.14 to 2.7.8
- [Release notes](https://github.com/vuejs/vue/releases)
- [Changelog](https://github.com/vuejs/vue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vuejs/vue/compare/v2.6.14...v2.7.8)

---
updated-dependencies:
- dependency-name: vue
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: vue-template-compiler
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-05 11:20:08 -04:00
CuriousMagpie cff6c5674f update: add styling 2022-08-04 17:26:04 -04:00
SabreCat d5c4e1666e 4.238.1 2022-08-04 16:19:03 -05:00
SabreCat 79071e3445 Merge branch 'develop' into release 2022-08-04 16:18:58 -05:00
Weblate 2ea707c27c Translated using Weblate (Korean)
Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 64.4% (1711 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (2558 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (367 of 367 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.2% (2551 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 96.1% (2549 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (752 of 755 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.4% (117 of 137 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2651 of 2651 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 98.9% (184 of 186 strings)

Translated using Weblate (Filipino)

Currently translated at 42.3% (1124 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (2633 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (751 of 755 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2651 of 2651 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (204 of 205 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (367 of 367 strings)

Translated using Weblate (Russian)

Currently translated at 96.1% (2548 of 2651 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2648 of 2651 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (136 of 137 strings)

Translated using Weblate (Russian)

Currently translated at 98.6% (212 of 215 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Catarina Rocha <caticalhau312@gmail.com>
Co-authored-by: Goggle K <afc731@gmail.com>
Co-authored-by: Hexe des Windes (she/her) <krausanna1@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: UNI <nibi727171@gmail.com>
Co-authored-by: Vince <vincemorel.vilan.889@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: datschka <datschka@gmx.at>
Co-authored-by: kat <memesarerealkool@gmail.com>
Co-authored-by: kat o(`ω´ )o <memesarerealkool@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/it/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2022-08-04 23:17:51 +02:00
Natalie L f7b727dc95 chore(content): add Woodland Wizard Achievement and Forest Friends Quest Bundle (#14159)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add Woodland Wizard achievement

* chore(content): add Forest Friends quest bundle

* fix(typo): whitespace

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-04 15:14:27 -05:00
CuriousMagpie edcb3f4289 update: making dropdown go with PROPS 2022-08-03 12:07:30 -04:00
CuriousMagpie bd28a282df updates: add demographic strings & drop-down and translated array code 2022-08-02 18:34:09 -04:00
SabreCat 9b9503b141 feat(tasks): make task copy/mirror pref per-group 2022-08-02 16:09:49 -05:00
CuriousMagpie 999071a15c still trying to get modal to work 2022-08-02 16:05:41 -04:00
SabreCat a78aea5456 Merge branch 'release' into develop 2022-08-02 10:10:00 -05:00
SabreCat 6e8e7318f3 4.238.0 2022-08-02 10:09:31 -05:00
SabreCat 1c3d4a6fd5 Revert "Revert "chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)""
This reverts commit 9a2b49b4bf.
2022-08-02 10:09:02 -05:00
CuriousMagpie 9bee9d0a06 working on editing code so the modal emits from group-plans instead of from within the file 2022-08-01 17:11:15 -04:00
SabreCat a418752041 Merge branch 'develop' into sabrecat/teams-rebase 2022-08-01 15:40:08 -05:00
CuriousMagpie f5b632e3e5 Merge remote-tracking branch 'upstream/develop' into group-tracking-modal 2022-08-01 15:55:00 -04:00
negue c9b3c48379 fixed gifting transaction / adding comments (#14150) 2022-08-01 11:10:00 -05:00
SabreCat 9ed2359c77 4.237.1 2022-08-01 08:51:49 -05:00
SabreCat 0dc21fa868 fix(css): redo sprites compile 2022-08-01 08:51:44 -05:00
Natalie L e2c6fb1ea2 chore(content): add August 2022 Backgrounds and Enchanted Armoire Items (#14149)
* chore(submodule): add August 2022 Mystery Items

* chore(content): August 2022 Backgrounds and Enchanted Armoire Items

* chore(submodule): August 2022 Backgrounds and Enchanted Armoire images

* fix(typo): space

* fix(whitespace): spaces

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-07-29 16:51:56 -05:00
SabreCat 7f8e44ff49 fix(groups): style and data sync fixes 2022-07-28 15:53:29 -05:00
CuriousMagpie 8ecd152b41 moved modal file from /groups into /group-plans 2022-07-27 18:25:56 -04:00
SabreCat e0e9381584 fix(groups): many, mostly style, fixes 2022-07-26 16:48:27 -05:00
CuriousMagpie ba22c18cd9 separating group plan creation modal out from groupPlan.vue 2022-07-26 16:59:53 -04:00
CuriousMagpie ef3767f80b chore(submodule): add August 2022 Mystery Items 2022-07-26 13:31:44 -04:00
SabreCat 18db432f7f feat(tasks): functional summary modal 2022-07-25 22:31:33 -05:00
SabreCat 30d3892fb4 Merge branch 'develop' into sabrecat/teams-rebase 2022-07-25 15:04:43 -05:00
Nishant Jain 8070486def add max length validations for summary in challenge create and update… (#14053)
* add max length validations for summary in challenge create and update controllers

* Add validation to group APIs

* fix lint errors

* add validation to group plan

* fix imports

* add tests

* add max length validations for summary in challenge create and update controllers

* Add validation to group APIs

* fix lint errors

* add validation to group plan

* fix imports

* add tests

* lint checks
2022-07-22 15:24:24 -05:00
SabreCat e06a0e5e7f WIP(tasks): new summary modal 2022-07-21 15:35:59 -05:00
SabreCat 153561dd42 Merge branch 'develop' into sabrecat/teams-rebase 2022-07-18 15:50:20 -05:00
SabreCat 7b067de4b9 chore(analytics): add tracking for task mirroring preference 2022-07-05 15:10:36 -05:00
SabreCat 9453b1269e Merge branch 'develop' into sabrecat/teams-rebase 2022-07-05 14:34:53 -05:00
SabreCat cf536a82f8 fix(teams): more style updates 2022-06-30 16:55:47 -05:00
SabreCat 5967e4356c fix(teams): style updates 2022-06-29 15:52:54 -05:00
SabreCat 6ebfa976fe Merge branch 'develop' into sabrecat/teams-rebase 2022-06-29 14:30:25 -05:00
SabreCat ddd5f20609 fix(teams): don't complain about move route when not moving 2022-06-29 09:06:08 -05:00
SabreCat a3f61306d3 feat(teams): user preference toggle for mirroring 2022-06-28 16:18:24 -05:00
SabreCat 712b85ce84 fix(teams): complete task sorting 2022-06-24 16:43:41 -05:00
SabreCat 9142588ba7 fix(tasks): better tasksOrder maintenance 2022-06-23 16:44:21 -05:00
SabreCat b657172a2b Merge branch 'develop' into sabrecat/teams-rebase 2022-06-21 13:50:50 -05:00
SabreCat b790b87ca8 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-21 09:34:17 -05:00
SabreCat 9d3059fc30 fix(teams): copy in assignee username during migration 2022-06-16 16:04:55 -05:00
SabreCat 647371accc Merge branch 'develop' into sabrecat/teams-rebase 2022-06-16 14:11:47 -05:00
SabreCat 8b084e627e WIP(teams): show open tasks on user view 2022-06-16 13:25:09 -05:00
SabreCat 4ac1a3e717 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-14 14:46:50 -05:00
SabreCat a0177fa44d WIP(teams): display assigned tasks on user's personal board 2022-06-13 16:53:29 -05:00
SabreCat 9fec111c4d Merge branch 'develop' into sabrecat/teams-rebase 2022-06-06 15:15:16 -05:00
SabreCat a559c1add8 refactor(tasks): get rid of behind-the-scenes task cloning 2022-06-03 16:40:09 -05:00
SabreCat 5868849034 Merge branch 'develop' into sabrecat/teams-rebase 2022-06-03 16:04:35 -05:00
SabreCat c98c7ab26c Merge branch 'develop' into sabrecat/teams-rebase 2022-05-31 15:51:16 -05:00
SabreCat 1ef7924ba5 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-24 09:39:01 -05:00
SabreCat 7651e6a540 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-19 14:30:29 -05:00
SabreCat bb20c44fde fix(needs-work): don't show Gold/Experience depletion for manager 2022-05-19 14:28:35 -05:00
SabreCat eb99ca0411 fix(cron): reset completions even if Daily wasn't due 2022-05-17 14:02:18 -05:00
SabreCat 9c24d43a13 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-17 09:34:51 -05:00
SabreCat bf9a7ea7d9 fix(teams): take user to relevant group from task assignment click 2022-05-13 14:26:14 -05:00
SabreCat ca1dbd2fc4 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-12 13:58:15 -05:00
SabreCat 04107ed6d3 fix(tasks): 12px padding, not 16 2022-05-10 15:36:43 -05:00
SabreCat 209b7bd1aa Merge branch 'develop' into sabrecat/teams-rebase 2022-05-10 13:58:14 -05:00
SabreCat db354875ee fix(teams): adjust task title spacing, don't damage user for team Daily 2022-05-06 14:23:09 -05:00
SabreCat 40af14b061 fix(multiassign): don't allow nonmanagers to uncheck from footer 2022-05-05 15:16:30 -05:00
SabreCat 1d048e0c35 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-05 13:56:36 -05:00
SabreCat 6a5f467a35 fix(teams): hover states, missing snackbars 2022-05-04 17:02:09 -05:00
SabreCat 86b0d6d86c fix(cron): handle when leader not found 2022-05-04 14:10:57 -05:00
SabreCat 565d33f6a7 Merge branch 'develop' into sabrecat/teams-rebase 2022-05-03 15:53:11 -05:00
SabreCat 588e5dd487 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-29 14:42:08 -05:00
SabreCat d8fbf9420e fix(teams): delete assignedUsers on open tasks 2022-04-28 15:48:40 -05:00
SabreCat edb606814c feat(teams): beta testing banner 2022-04-22 15:47:55 -05:00
SabreCat 32823e3760 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-21 14:10:27 -05:00
SabreCat 48f5ffc997 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-15 14:26:19 -05:00
SabreCat 90375e3bc4 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-14 15:36:26 -05:00
SabreCat c4a92ba384 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-12 14:48:45 -05:00
SabreCat 8f7e5d544e Merge branch 'develop' into sabrecat/teams-rebase 2022-04-07 14:55:16 -05:00
SabreCat 40113b0458 Merge branch 'develop' into sabrecat/teams-rebase 2022-04-05 13:56:25 -05:00
SabreCat 48be0a38bf Merge branch 'develop' into sabrecat/teams-rebase 2022-03-31 14:49:35 -05:00
SabreCat 459b327e2d feat(nav): clicking "Group" goes to first group 2022-03-30 15:53:48 -05:00
SabreCat d3fde93762 fix(cron): don't adjust task decay by "assignments" for open Daily 2022-03-30 13:06:50 -05:00
SabreCat aa81c330af Merge branch 'develop' into sabrecat/teams-rebase 2022-03-30 11:35:33 -05:00
SabreCat 7283d112f4 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-29 14:10:18 -05:00
SabreCat f1b0aa2e7c Merge branch 'develop' into sabrecat/teams-rebase 2022-03-28 16:34:31 -05:00
SabreCat 354f3578a2 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-23 20:23:29 -05:00
SabreCat c5ef803458 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-22 16:29:47 -05:00
SabreCat 40801c0d32 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-15 15:05:25 -05:00
SabreCat e9ca17bbd8 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-14 14:33:36 -05:00
SabreCat 5a638ab4b8 Merge branch 'develop' into sabrecat/teams-rebase 2022-03-09 16:37:31 -06:00
SabreCat 7fa4e6f791 fix(teams): don't show status footer for Habits or Rewards 2022-03-09 11:24:19 -06:00
SabreCat 61be42bf05 WIP(teams): data migration draft 2022-03-04 18:17:43 -06:00
SabreCat 9d4bf22720 fix(teams): Close button and padding fix 2022-03-04 15:15:50 -06:00
SabreCat 24349bed0a fix(teams): display and logic adjustments 2022-03-01 16:18:57 -06:00
SabreCat 17b93322aa fix(teams): reload completed To Do 2022-02-28 16:28:54 -06:00
SabreCat ef6d92e7af Merge branch 'develop' into sabrecat/teams-rebase 2022-02-28 09:50:07 -06:00
SabreCat 35ed158dd9 WIP(teams): updated completion states
and fixed an issue with cron saving
2022-02-24 16:23:46 -06:00
SabreCat 31cbcf53a2 Merge branch 'develop' into sabrecat/teams-rebase 2022-02-24 14:54:34 -06:00
SabreCat 3a2fd28199 WIP(tasks): start task refresh process 2022-02-22 16:50:35 -06:00
SabreCat 1473408752 Merge branch 'develop' into sabrecat/teams-rebase 2022-02-22 12:18:59 -06:00
SabreCat 53babfb9fe WIP(teams): snakey checkmark 2022-02-18 17:41:32 -06:00
SabreCat 64694c3a29 WIP(teams): partial single-assign Dailies styling 2022-02-17 22:00:45 -06:00
SabreCat 1bd2ec0463 fix(branch): reset package files to develop 2022-02-17 16:07:53 -06:00
SabreCat 4e9625454c WIP(teams): new single assign footer states for To Do's 2022-02-17 15:52:59 -06:00
SabreCat a58dd35fbe WIP(teams): tiny checkmarks 2022-02-17 15:52:59 -06:00
SabreCat 54088f5374 WIP(teams): fixes from demo session 2022-02-17 15:52:59 -06:00
SabreCat 9eebcf9b16 fix(deps): bad babel-eslint version? 2022-02-17 15:52:58 -06:00
SabreCat 44722a0d4c chore(deps): update package locks 2022-02-17 15:52:58 -06:00
SabreCat 7eae0a83f9 4.221.2 2022-02-17 15:52:40 -06:00
SabreCat 294b94206f fix(tz): remove moment-timezone completely 2022-02-17 15:51:55 -06:00
SabreCat 7e73c336dd WIP(teams): stylish and functional multi assign checkboxes 2022-02-17 15:51:28 -06:00
SabreCat 82d3545c08 WIP(teams): various fixes 2022-02-17 15:51:28 -06:00
SabreCat e312ea943f WIP(teams): partial style implementation for status rows 2022-02-17 15:51:27 -06:00
SabreCat a4a1595ec7 WIP(teams): very janky multi status 2022-02-17 15:51:27 -06:00
SabreCat 4b07e3a116 WIP(teams): start of multi state implementation 2022-02-17 15:51:27 -06:00
SabreCat 13e645fa4b fix(teams): correct some unassignment bugs 2022-02-17 15:51:27 -06:00
SabreCat 0d876472a3 WIP(teams): fix initial assignment sync, add Daily handling 2022-02-17 15:51:26 -06:00
SabreCat 9e527f4f35 WIP(teams): can do To Do's 2022-02-17 15:51:09 -06:00
SabreCat eaa5f821a4 WIP(multi-assign): functioning multi 2022-02-17 15:50:44 -06:00
SabreCat a495db8480 WIP(assignment): change to object style
to do--let assignment API accept an array
2022-02-17 15:50:42 -06:00
SabreCat fa99458ca4 WIP(multiassign): resume shared completion implementation 2022-02-17 15:50:23 -06:00
Sabe Jones 1f81e1971b fix(cron): remove now redundant logic 2022-02-17 15:49:56 -06:00
Sabe Jones 45fc2b62e3 fix(cron): process team board tasks assigned to the user 2022-02-17 15:49:56 -06:00
Sabe Jones f843564444 fix(storybook): temporarily disable story 2022-02-17 15:49:55 -06:00
Sabe Jones fb216fba8e fix(cron): reset checklists as needed 2022-02-17 15:49:54 -06:00
Sabe Jones 603cc93957 fix(ui): further cursor tweaks for Teams 2022-02-17 15:49:54 -06:00
Sabe Jones 63e0875f32 fix(teams): allow ticking team checklists 2022-02-17 15:49:34 -06:00
Sabe Jones 029e41472f fix(teams): filter further to hide team tasks, don't override 2022-02-17 15:49:19 -06:00
Sabe Jones 1752c08fd9 fix(teams): allow managers to reorder tasks 2022-02-17 15:49:19 -06:00
Sabe Jones 395d9e7650 fix(teams): maybe goodish style? 2022-02-17 15:49:18 -06:00
Sabe Jones 61d396204f fix(teams): more layout tweakage 2022-02-17 15:49:17 -06:00
Sabe Jones f233c511cc fix(teams): layout issues, error, change timezone format 2022-02-17 15:49:16 -06:00
Sabe Jones 0806391ab8 feat(teams): Day Start and adjust uncheck wording 2022-02-17 15:48:55 -06:00
Sabe Jones dcaba7f186 fix(teams): update beta banner wording 2022-02-17 15:47:55 -06:00
Sabe Jones 59dc97b75f WIP(teams): fixes, beta banner 2022-02-17 15:47:37 -06:00
Sabe Jones 85a9ea726c WIP(teams): draft of server literal-actual-cron script 2022-02-17 15:47:23 -06:00
Sabe Jones d53813adc7 WIP(teams): add some analytics, remove extraneous logic 2022-02-17 15:47:02 -06:00
Sabe Jones 221dd7a81e fix(tasks): more lock icon revision, no error on manager uncheck 2022-02-17 15:46:45 -06:00
Sabe Jones 99bf6349e1 fix(assignment): update single select dropdown style 2022-02-17 15:46:44 -06:00
Sabe Jones 801b902bb8 fix(tasks): manager lock icon, coerce task value to Number 2022-02-17 15:46:43 -06:00
Sabe Jones 072b09e030 feat(teams): quick add
Also fixes issue with lock icons
Also hides task copies from personal task board
2022-02-17 15:46:42 -06:00
Sabe Jones a5680836bd feat(teams): new disapproval workflow, managers can uncheck tasks 2022-02-17 15:46:23 -06:00
Sabe Jones bb9ba61d12 feat(teams): show team name 2022-02-17 15:44:58 -06:00
Sabe Jones a88f97831a fix(teams): update single select style and correct create/edit issue 2022-02-17 15:44:58 -06:00
Sabe Jones ae0528e5cd WIP(teams): fix initially unassigned task, add completedBy data 2022-02-17 15:44:57 -06:00
Sabe Jones 74345adf6b fix(teams): make selectSingle workflows functional 2022-02-17 15:44:56 -06:00
negue 7dbee4caed clone selectMulti to selectSingle for assignedMember 2022-02-17 15:44:55 -06:00
Sabe Jones f4feb09fbc fix(cron): actually process cron for assigned tasks 2022-02-17 15:44:54 -06:00
Sabe Jones 6cddb3bf82 WIP(teams): more partial fixing 2022-02-17 15:44:36 -06:00
Sabe Jones 248e1c6fe9 WIP(teams): reimplement open tasking 2022-02-17 15:43:28 -06:00
Sabe Jones 6e39c79cff WIP(teams): simplify task footers 2022-02-17 15:41:07 -06:00
Sabe Jones 5bf4e18ce8 WIP(teams): don't damage leader for incomplete team Dailies 2022-02-17 15:41:06 -06:00
Sabe Jones cab4a2a8fa WIP(teams): begin simplification 2022-02-17 15:41:05 -06:00
510 changed files with 15698 additions and 10572 deletions
+1
View File
@@ -2,6 +2,7 @@
"name": "Habitica V3 API Documentation",
"title": "Habitica",
"url": "https://habitica.com",
"version": "3.0.0",
"sampleUrl": null,
"header": {
"title": "Introduction",
@@ -0,0 +1,97 @@
/* eslint-disable no-console */
import { model as UserModel } from '../../../website/server/models/user';
import { TransactionModel } from '../../../website/server/models/transaction';
const MIGRATION_NAME = '20220915_transactions_user_name';
/* transaction config */
const transactionPerRun = 500;
const progressCount = 1000;
const transactionQuery = {
migration: { $ne: MIGRATION_NAME }, // skip already migrated entries
'transactionType': { $in: ['gift_send', 'gift_receive'] },
};
let count = 0;
async function updateTransaction (transaction, userNameMap) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (userNameMap.has(transaction.reference)) {
set['referenceText'] = userNameMap.get(transaction.reference);
} else {
set['referenceText'] = 'Account not found';
}
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
return TransactionModel.updateOne({
_id: transaction._id
}, { $set: set }).exec();
}
export default async function processTransactions () {
const fields = {
_id: 1,
reference: 1,
referenceText: 1,
};
const userNameMap = new Map();
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await TransactionModel // eslint-disable-line no-await-in-loop
.find(transactionQuery)
.limit(transactionPerRun)
.sort({reference: 1})
.select(fields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
}
// check for unknown users and load the names
const userIdsToLoad = [];
for (const foundTransaction of foundTransactions) {
const userId = foundTransaction.reference;
if (userNameMap.has(userId)) {
continue;
}
userIdsToLoad.push(userId);
}
const users = await UserModel // eslint-disable-line no-await-in-loop
.find({
_id: { $in: userIdsToLoad }
})
.select({
_id: 1,
'auth.local.username': 1,
})
.lean()
.exec();
for (const user of users) {
const localUserName = user.auth?.local?.username;
if (!localUserName) {
console.warn(`\nNo Username found for ID: ${user._id}\n`);
continue;
}
userNameMap.set(user._id, localUserName)
}
await Promise.all(foundTransactions.map(t => updateTransaction(t, userNameMap))); // eslint-disable-line no-await-in-loop
}
};
+71
View File
@@ -0,0 +1,71 @@
import filter from 'lodash/filter';
import find from 'lodash/find';
import isArray from 'lodash/isArray';
import { model as Group } from '../../website/server/models/group';
import { model as User } from '../../website/server/models/user';
import * as Tasks from '../../website/server/models/task';
async function updateTeamTasks (team) {
const toSave = [];
const teamTasks = await Tasks.Task.find({
'group.id': team._id,
}).exec();
const teamBoardTasks = filter(teamTasks, task => !task.userId);
const teamUserTasks = filter(teamTasks, task => task.userId);
for (const boardTask of teamBoardTasks) {
if (isArray(boardTask.group.assignedUsers)) {
boardTask.group.approval = undefined;
boardTask.group.assignedDate = undefined;
boardTask.group.assigningUsername = undefined;
boardTask.group.sharedCompletion = undefined;
for (const assignedUserId of boardTask.group.assignedUsers) {
const assignedUser = await User.findById(assignedUserId, 'auth'); // eslint-disable-line no-await-in-loop
const userTask = find(teamUserTasks, task => task.userId === assignedUserId
&& task.group.taskId === boardTask._id);
if (!boardTask.group.assignedUsersDetail) boardTask.group.assignedUsersDetail = {};
if (userTask && assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: userTask.group.assignedDate,
assignedUsername: assignedUser.auth.local.username,
assigningUsername: userTask.group.assigningUsername,
completed: userTask.completed || false,
completedDate: userTask.dateCompleted,
};
} else if (assignedUser) {
boardTask.group.assignedUsersDetail[assignedUserId] = {
assignedDate: new Date(),
assignedUsername: assignedUser.auth.local.username,
assigningUsername: null,
completed: false,
completedDate: null,
};
} else {
const taskIndex = boardTask.group.assignedUsers.indexOf(assignedUserId);
boardTask.group.assignedUsers.splice(taskIndex, 1);
}
if (userTask) toSave.push(Tasks.Task.findByIdAndDelete(userTask._id));
}
boardTask.markModified('group');
toSave.push(boardTask.save());
}
}
return Promise.all(toSave);
}
export default async function processTeams () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const taskPromises = activeTeams.map(updateTeamTasks);
return Promise.all(taskPromises);
}
+1175 -700
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,19 +1,19 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.237.0",
"version": "4.245.2",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.18.6",
"@babel/preset-env": "^7.18.6",
"@babel/register": "^7.18.6",
"@babel/core": "^7.18.13",
"@babel/preset-env": "^7.19.1",
"@babel/register": "^7.18.9",
"@google-cloud/trace-agent": "^5.1.6",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.52.0",
"apidoc": "^0.53.0",
"apple-auth": "^1.0.7",
"bcrypt": "^5.0.1",
"body-parser": "^1.20.0",
@@ -61,7 +61,7 @@
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.3.7",
"rate-limiter-flexible": "^2.3.10",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.9",
"remove-markdown": "^0.5.0",
@@ -121,7 +121,7 @@
"mocha": "^5.1.1",
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.6",
"run-rs": "^0.7.7",
"sinon": "^13.0.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
+105
View File
@@ -0,0 +1,105 @@
import forEach from 'lodash/forEach';
import { model as Group } from '../website/server/models/group';
import { model as User } from '../website/server/models/user';
import * as Tasks from '../website/server/models/task';
import { daysSince, shouldDo } from '../website/common/script/cron';
const TASK_VALUE_CHANGE_FACTOR = 0.9747;
const MIN_TASK_VALUE = -47.27;
async function updateTeamTasks (team) {
const toSave = [];
let teamLeader = await User.findOne({ _id: team.leader }, 'preferences').exec();
if (!teamLeader) { // why would this happen?
teamLeader = {
preferences: { }, // when options are sanitized this becomes CDS 0 at UTC
};
}
if (
!team.cron || !team.cron.lastProcessed
|| daysSince(team.cron.lastProcessed, teamLeader.preferences) > 0
) {
const tasks = await Tasks.Task.find({
'group.id': team._id,
userId: { $exists: false },
$or: [
{ type: 'todo', completed: false },
{ type: { $in: ['habit', 'daily'] } },
],
}).exec();
const tasksByType = {
habits: [], dailys: [], todos: [], rewards: [],
};
forEach(tasks, task => tasksByType[`${task.type}s`].push(task));
forEach(tasksByType.habits, habit => {
if (!(habit.up && habit.down) && habit.value !== 0) {
habit.value *= 0.5;
if (Math.abs(habit.value) < 0.1) habit.value = 0;
toSave.push(habit.save());
}
});
forEach(tasksByType.todos, todo => {
if (!todo.completed) {
const delta = TASK_VALUE_CHANGE_FACTOR ** todo.value;
todo.value -= delta;
if (todo.value < MIN_TASK_VALUE) todo.value = MIN_TASK_VALUE;
toSave.push(todo.save());
}
});
forEach(tasksByType.dailys, daily => {
let processChecklist = false;
let assignments = 0;
let completions = 0;
for (const assignedUser in daily.group.assignedUsersDetail) {
if (Object.prototype.hasOwnProperty.call(daily.group.assignedUsersDetail, assignedUser)) {
assignments += 1;
if (daily.group.assignedUsersDetail[assignedUser].completed) {
completions += 1;
daily.group.assignedUsersDetail[assignedUser].completed = false;
}
}
}
if (completions > 0) daily.markModified('group.assignedUsersDetail');
if (daily.completed) {
processChecklist = true;
daily.completed = false;
} else if (shouldDo(team.cron.lastProcessed, daily, teamLeader.preferences)) {
processChecklist = true;
const delta = TASK_VALUE_CHANGE_FACTOR ** daily.value;
if (assignments > 0) {
daily.value -= ((completions / assignments) * delta);
}
if (daily.value < MIN_TASK_VALUE) daily.value = MIN_TASK_VALUE;
}
daily.isDue = shouldDo(new Date(), daily, teamLeader.preferences);
if (processChecklist && daily.checklist.length > 0) {
daily.checklist.forEach(i => { i.completed = false; });
}
toSave.push(daily.save());
});
if (!team.cron) team.cron = {};
team.cron.lastProcessed = new Date();
toSave.push(team.save());
}
return Promise.all(toSave);
}
export default async function processTeamsCron () {
const activeTeams = await Group.find({
'purchased.plan.customerId': { $exists: true },
$or: [
{ 'purchased.plan.dateTerminated': { $exists: false } },
{ 'purchased.plan.dateTerminated': null },
{ 'purchased.plan.dateTerminated': { $gt: new Date() } },
],
}).exec();
const cronPromises = activeTeams.map(updateTeamTasks);
return Promise.all(cronPromises);
}
-25
View File
@@ -13,11 +13,6 @@ function getUser () {
username: 'username',
email: 'email@email',
},
facebook: {
emails: [{
value: 'email@facebook',
}],
},
google: {
emails: [{
value: 'email@google',
@@ -62,30 +57,12 @@ describe('emails', () => {
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [facebook users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [google users]', () => {
const attachEmail = requireAgain(pathToEmailLib);
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook.emails;
delete user.auth.apple.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
@@ -103,7 +80,6 @@ describe('emails', () => {
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.google.emails;
delete user.auth.facebook.emails;
const data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
@@ -118,7 +94,6 @@ describe('emails', () => {
const { getUserInfo } = attachEmail;
const user = getUser();
delete user.auth.local.email;
delete user.auth.facebook;
delete user.auth.google;
delete user.auth.apple;
+1 -1
View File
@@ -246,7 +246,7 @@ describe('Password Utilities', () => {
it('returns false if the user has no local auth', async () => {
const user = await generateUser({
auth: {
facebook: {},
google: {},
},
});
const res = await validatePasswordResetCodeAndFindUser(encrypt(JSON.stringify({
+5 -3
View File
@@ -672,10 +672,12 @@ describe('payments/index', () => {
context('No Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEvent').returns(null);
sinon.stub(worldState, 'getCurrentEventList').returns([]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('does not apply a discount', async () => {
@@ -692,14 +694,14 @@ describe('payments/index', () => {
context('Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEvent').returns({
sinon.stub(worldState, 'getCurrentEventList').returns([{
...common.content.events.fall2020,
event: 'fall2020',
});
}]);
});
afterEach(() => {
worldState.getCurrentEvent.restore();
worldState.getCurrentEventList.restore();
});
it('applies a discount', async () => {
+11 -210
View File
@@ -1,12 +1,10 @@
import { each, find, findIndex } from 'lodash';
import { model as Challenge } from '../../../../website/server/models/challenge';
import { each, findIndex } from 'lodash';
import { model as Group } from '../../../../website/server/models/group';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
describe('Group Task Methods', () => {
let guild; let leader; let challenge; let
task;
let guild; let leader; let task;
const tasksToTest = {
habit: {
text: 'test habit',
@@ -31,10 +29,6 @@ describe('Group Task Methods', () => {
},
};
function findLinkedTask (updatedLeadersTask) {
return updatedLeadersTask.group.taskId === task._id;
}
beforeEach(async () => {
guild = new Group({
name: 'test party',
@@ -47,19 +41,9 @@ describe('Group Task Methods', () => {
guild.leader = leader._id;
challenge = new Challenge({
name: 'Test Challenge',
shortName: 'Test',
leader: leader._id,
group: guild._id,
});
leader.challenges = [challenge._id];
await Promise.all([
guild.save(),
leader.save(),
challenge.save(),
]);
});
@@ -78,7 +62,15 @@ describe('Group Task Methods', () => {
});
it('syncs an assigned task to a user', async () => {
await guild.syncTask(task, leader);
await guild.syncTask(task, [leader], leader);
const updatedTask = await Tasks.Task.findOne({ _id: task._id });
expect(updatedTask.group.assignedUsers).to.contain(leader._id);
expect(updatedTask.group.assignedUsersDetail[leader._id]).to.exist;
});
it('creates tags for a user when task is synced', async () => {
await guild.syncTask(task, [leader], leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const tagIndex = findIndex(updatedLeader.tags, { id: guild._id });
@@ -88,197 +80,6 @@ describe('Group Task Methods', () => {
expect(newTag.name).to.equal(guild.name);
expect(newTag.group).to.equal(guild._id);
});
it('create tags for a user when task is synced', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
});
it('syncs updated info for assigned task to a user', async () => {
await guild.syncTask(task, leader);
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
});
it('syncs checklist items to an assigned user', async () => {
await guild.syncTask(task, leader);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
if (task.type !== 'daily' && task.type !== 'todo') return;
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
});
describe('syncs updated info', async () => {
let newMember;
beforeEach(async () => {
newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await guild.syncTask(task, leader);
await guild.syncTask(task, newMember);
});
it('syncs updated info for assigned task to all users', async () => {
const updatedTaskName = 'Update Task name';
task.text = updatedTaskName;
task.group.approval.required = true;
await guild.updateTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(task.group.assignedUsers).to.contain(leader._id);
expect(syncedTask).to.exist;
expect(syncedTask.text).to.equal(task.text);
expect(syncedTask.group.approval.required).to.equal(true);
expect(task.group.assignedUsers).to.contain(newMember._id);
expect(syncedMemberTask).to.exist;
expect(syncedMemberTask.text).to.equal(task.text);
expect(syncedMemberTask.group.approval.required).to.equal(true);
});
it('syncs a new checklist item to all assigned users', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const newCheckListItem = {
text: 'Checklist Item 1',
completed: false,
};
task.checklist.push(newCheckListItem);
await guild.updateTask(task, { newCheckListItem });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
});
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
const updateCheckListText = 'Updated checklist item';
if (task.checklist) {
task.checklist[0].text = updateCheckListText;
}
await guild.updateTask(task, { updateCheckListItems: [task.checklist[0]] });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
});
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
if (task.type !== 'daily' && task.type !== 'todo') return;
await guild.updateTask(task, { removedCheckListItemId: task.checklist[0].id });
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
const updatedMember = await User.findOne({ _id: newMember._id });
const updatedMemberTasks = await Tasks.Task.find({ _id: { $in: updatedMember.tasksOrder[`${taskType}s`] } });
const syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
expect(syncedTask.checklist.length).to.equal(0);
expect(syncedMemberTask.checklist.length).to.equal(0);
});
});
it('removes assigned tasks when master task is deleted', async () => {
await guild.syncTask(task, leader);
await guild.removeTask(task);
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ userId: leader._id, type: taskType });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(updatedLeader.tasksOrder[`${taskType}s`]).to.not.include(task._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and deletes group tasks for a user when remove-all is specified', async () => {
await guild.syncTask(task, leader);
await guild.unlinkTask(task, leader, 'remove-all');
const updatedLeader = await User.findOne({ _id: leader._id });
const updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(syncedTask).to.not.exist;
});
it('unlinks and keeps group tasks for a user when keep-all is specified', async () => {
await guild.syncTask(task, leader);
let updatedLeader = await User.findOne({ _id: leader._id });
let updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const syncedTask = find(updatedLeadersTasks, findLinkedTask);
await guild.unlinkTask(task, leader, 'keep-all');
updatedLeader = await User.findOne({ _id: leader._id });
updatedLeadersTasks = await Tasks.Task.find({ _id: { $in: updatedLeader.tasksOrder[`${taskType}s`] } });
const updatedSyncedTask = find(
updatedLeadersTasks,
updatedLeadersTask => updatedLeadersTask._id === syncedTask._id,
);
expect(task.group.assignedUsers).to.not.contain(leader._id);
expect(updatedSyncedTask).to.exist;
expect(updatedSyncedTask.group._id).to.be.undefined;
});
});
});
});
+13 -6
View File
@@ -246,13 +246,23 @@ describe('Task Model', () => {
expect(foundTasks[0].text).to.eql(taskWithAlias.text);
});
it('scopes alias lookup to user', async () => {
it('scopes alias lookup to user when querying aliases only', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
alias: { $in: [taskWithAlias.alias] },
userId: user._id,
});
});
it('scopes alias lookup to user when querying aliases and IDs', async () => {
await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias, secondTask._id], user._id);
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
$or: [
{ _id: { $in: [] } },
{ _id: { $in: [secondTask._id] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
userId: user._id,
@@ -270,10 +280,7 @@ describe('Task Model', () => {
expect(Tasks.Task.find).to.be.calledOnce;
expect(Tasks.Task.find).to.be.calledWithMatch({
$or: [
{ _id: { $in: [] } },
{ alias: { $in: [taskWithAlias.alias] } },
],
alias: { $in: [taskWithAlias.alias] },
userId: user._id,
foo: 'bar',
});
@@ -4,6 +4,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('POST /challenges', () => {
it('returns error when group is empty', async () => {
@@ -60,6 +61,22 @@ describe('POST /challenges', () => {
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({
members: 1,
});
await expect(user.post('/challenges', {
group: group._id,
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
context('Creating a challenge for a valid group', () => {
let groupLeader;
let group;
@@ -4,6 +4,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_CHALLENGES } from '../../../../../website/common/script/constants';
describe('PUT /challenges/:challengeId', () => {
let privateGuild; let user; let nonMember; let challenge; let
@@ -91,4 +92,15 @@ describe('PUT /challenges/:challengeId', () => {
expect(res.name).to.equal('New Challenge Name');
expect(res.description).to.equal('New challenge description.');
});
it('return error when challenge summary is greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
await expect(user.put(`/challenges/${challenge._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -11,10 +10,6 @@ describe('POST /group/:groupId/remove-manager', () => {
const groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
@@ -63,28 +58,4 @@ describe('POST /group/:groupId/remove-manager', () => {
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
const task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${nonManager._id}`);
const memberTasks = await nonManager.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await nonManager.post(`/tasks/${syncedTask._id}/score/up`);
const updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(1); // user gets mystery items
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});
@@ -3,6 +3,7 @@ import {
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => {
let user;
@@ -71,6 +72,20 @@ describe('POST /group', () => {
expect(updatedGroup.summary).to.eql(summary);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const name = 'Test Group';
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', {
name,
type: 'guild',
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
context('Guilds', () => {
@@ -3,6 +3,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('PUT /group', () => {
let leader; let nonLeader; let groupToUpdate; let
@@ -130,4 +131,15 @@ describe('PUT /group', () => {
expect(response.bannedWordsAllowed).to.eql(undefined);
});
it('returns error when summary is longer than MAX_SUMMARY_SIZE_FOR_GUILDS characters', async () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
});
@@ -13,6 +13,9 @@ describe('POST /tasks/clearCompletedTodos', () => {
{ 'purchased.plan.customerId': 'group-unlimited' },
);
const challenge = await generateChallenge(user, guild);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post(`/challenges/${challenge._id}/join`);
const initialTodoCount = user.tasksOrder.todos.length;
@@ -33,7 +36,7 @@ describe('POST /tasks/clearCompletedTodos', () => {
text: 'todo 7',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
const tasks = await user.get('/tasks/user?type=todos');
expect(tasks.length).to.equal(initialTodoCount + 7);
@@ -30,7 +30,7 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
expect(savedTask.checklist[0].completed).to.equal(true);
});
it('can use a alias to score a checklist item', async () => {
it('can use an alias to score a checklist item', async () => {
const task = await user.post('/tasks/user', {
type: 'daily',
text: 'Daily with checklist',
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
translate as t,
createAndPopulateGroup,
@@ -8,10 +7,6 @@ describe('Groups DELETE /tasks/:id', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -35,8 +30,7 @@ describe('Groups DELETE /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
});
it('deletes a group task', async () => {
@@ -64,81 +58,4 @@ describe('Groups DELETE /tasks/:id', () => {
message: t('messageTaskNotFound'),
});
});
it('removes deleted taskʾs approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await user.put(`/tasks/${task._id}/`, {
requiresApproval: true,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3); // mystery items
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(3);
expect(member2.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
await member2.del(`/tasks/${task._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(member2.notifications.length).to.equal(2);
});
it('deletes task from assigned user', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask).to.not.exist;
});
it('deletes task from all assigned users', async () => {
await user.del(`/tasks/${task._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask).to.not.exist;
expect(member2SyncedTask).to.not.exist;
});
it('prevents a user from deleting a task they are assigned to', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await expect(member.del(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDeleteAssignedGroupTasks'),
});
});
it('allows a user to delete a task after leaving a group', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/groups/${guild._id}/leave`);
await member.del(`/tasks/${syncedTask._id}`);
await expect(member.get(`/tasks/${syncedTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: 'Task not found.',
});
});
});
@@ -1,78 +0,0 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
describe('GET /approvals/group/:groupId', () => {
let user; let guild; let member; let addlMember; let task; let syncedTask; let
addlSyncedTask;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
addlMember = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${addlMember._id}`);
const memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
const addlMemberTasks = await addlMember.get('/tasks/user');
addlSyncedTask = find(addlMemberTasks, findAssignedTask);
try {
await member.post(`/tasks/${syncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
try {
await addlMember.post(`/tasks/${addlSyncedTask._id}/score/up`);
} catch (e) {
// eslint-disable-next-line no-empty
}
});
it('provides only user\'s own tasks when user is not the group leader', async () => {
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]).to.not.exist;
});
it('allows group leaders to get a list of tasks that need approval', async () => {
const approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
it('allows managers to get a list of tasks that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
const approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
expect(approvals[1]._id).to.equal(addlSyncedTask._id);
});
});
@@ -1,261 +0,0 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
describe('POST /tasks/:id/approve/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
},
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
member2 = members[1]; // eslint-disable-line prefer-destructuring
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
});
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(member.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('approves an assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await member.sync();
expect(member.notifications.length).to.equal(3);
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
memberTasks = await member.get('/tasks/user');
syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(2);
expect(member2.notifications.length).to.equal(1);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents approving a task if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
it('completes master task when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is approved', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
await member2.post(`/tasks/${member2SyncedTask._id}/score/up`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
expect(masterTask.completed).to.equal(true);
});
});
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup,
translate as t,
@@ -8,10 +7,6 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -37,14 +32,15 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
it('errors when user is not assigned', async () => {
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
code: 400,
error: 'BadRequest',
message: 'Task not completed by this user.',
});
});
it('errors when user is not the group leader', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await member.post(`/tasks/${task._id}/score/up`);
await expect(member.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
@@ -54,132 +50,64 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
});
it('marks a task as needing more work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
// score task to require approval
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
await user.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
// Check that the notification is correct
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = user.profile.name;
const taskText = task.text;
const managerName = user.auth.local.username;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(user._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await user.sync();
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('allows a manager to mark a task as needing work', async () => {
await member.sync();
const initialNotifications = member.notifications.length;
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
// score task to require approval
await member.post(`/tasks/${syncedTask._id}/score/up`);
const initialNotifications = member.notifications.length;
await member.post(`/tasks/${task._id}/score/up`);
await member2.post(`/tasks/${task._id}/needs-work/${member._id}`);
[memberTasks] = await Promise.all([member.get('/tasks/user'), member.sync()]);
syncedTask = find(memberTasks, findAssignedTask);
// Check that the notification approval request has been removed
expect(syncedTask.group.approval.requested).to.equal(false);
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
await member.sync();
expect(member.notifications.length).to.equal(initialNotifications + 3);
const notification = member.notifications[member.notifications.length - 1];
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
const taskText = syncedTask.text;
const managerName = member2.profile.name;
const taskText = task.text;
const managerName = member2.auth.local.username;
expect(notification.data.message).to.equal(t('taskNeedsWork', { taskText, managerName }));
expect(notification.data.task.id).to.equal(syncedTask._id);
expect(notification.data.task.id).to.equal(task._id);
expect(notification.data.task.text).to.equal(taskText);
expect(notification.data.group.id).to.equal(syncedTask.group.id);
expect(notification.data.group.id).to.equal(task.group.id);
expect(notification.data.group.name).to.equal(guild.name);
expect(notification.data.manager.id).to.equal(member2._id);
expect(notification.data.manager.name).to.equal(managerName);
// Check that the managers' GROUP_TASK_APPROVAL notifications have been removed
await Promise.all([user.sync(), member2.sync()]);
expect(user.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
expect(member2.notifications.find(n => { // eslint-disable-line arrow-body-style
return n.data.taskId === syncedTask._id && n.type === 'GROUP_TASK_APPROVAL';
})).to.equal(undefined);
});
it('prevents marking a task as needing work if it was already approved', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
it('prevents marking a task as needing work if it is not waiting for approval', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await expect(user.post(`/tasks/${task._id}/needs-work/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalWasNotRequested'),
});
});
});
@@ -8,10 +8,6 @@ describe('POST /tasks/:id/score/:direction', () => {
let user; let guild; let member; let member2; let
task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -30,209 +26,50 @@ describe('POST /tasks/:id/score/:direction', () => {
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
});
it('prevents user from scoring a task that needs to be approved', async () => {
await user.update({
'preferences.language': 'cs',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(updatedTask.group.approval.requested).to.equal(true);
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const direction = 'up';
await member.post(`/tasks/${syncedTask._id}/score/${direction}`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(3);
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
direction,
}));
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const response = await member.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskRequiresApproval'));
});
it('allows a user to score an approved task', async () => {
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await user.post(`/tasks/${task._id}/approve/${member._id}`);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const updatedTask = await member.get(`/tasks/${syncedTask._id}`);
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('completes master task when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
it('completes single-assigned task', async () => {
await member.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(true);
expect(sourceTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is completed', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
it('errors when task has already been completed', async () => {
await member.post(`/tasks/${task._id}/score/up`);
await expect(member.post(`/tasks/${task._id}/score/up`)).to.be.rejected.and.to.eventually.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
const member2Tasks = await member2.get('/tasks/user');
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
it('does not complete multi-assigned task when not all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(false);
expect(sourceTask.completed).to.equal(false);
});
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
const sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
it('completes multi-assigned task when all assignees have completed', async () => {
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
const memberTasks = await member.get('/tasks/user');
const member2Tasks = await member2.get('/tasks/user');
const syncedTask = find(
memberTasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
const syncedTask2 = find(
member2Tasks,
memberTask => memberTask.group.taskId === sharedCompletionTask._id,
);
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
await member.post(`/tasks/${task._id}/score/up`);
await member2.post(`/tasks/${task._id}/score/up`);
const groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
const masterTask = find(groupTasks, groupTask => groupTask._id === sharedCompletionTask._id);
const sourceTask = find(groupTasks, groupTask => groupTask._id === task._id);
expect(masterTask.completed).to.equal(true);
expect(sourceTask.completed).to.equal(true);
});
});
@@ -39,7 +39,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when task is not found', async () => {
await expect(user.post(`/tasks/${generateUUID()}/assign/${member._id}`))
await expect(user.post(`/tasks/${generateUUID()}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -56,7 +56,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
notes: 1976,
});
await expect(user.post(`/tasks/${nonGroupTask._id}/assign/${member._id}`))
await expect(user.post(`/tasks/${nonGroupTask._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -67,7 +67,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
it('returns error when user is not a member of the group', async () => {
const nonUser = await generateUser();
await expect(nonUser.post(`/tasks/${task._id}/assign/${member._id}`))
await expect(nonUser.post(`/tasks/${task._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@@ -76,7 +76,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('returns error when non leader tries to create a task', async () => {
await expect(member2.post(`/tasks/${task._id}/assign/${member._id}`))
await expect(member2.post(`/tasks/${task._id}/assign`, [member._id]))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
@@ -84,49 +84,23 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
});
it('allows user to assign themselves (claim)', async () => {
await member.post(`/tasks/${task._id}/assign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
it('sends notifications to group leader and managers when a task is claimed', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member.post(`/tasks/${task._id}/assign/${member._id}`);
await user.sync();
await member2.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
expect(user.notifications.length).to.equal(3); // includes Guild Joined achievement
expect(user.notifications[2].type).to.equal('GROUP_TASK_CLAIMED');
expect(user.notifications[2].data.taskId).to.equal(groupTask[0]._id);
expect(user.notifications[2].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(2);
expect(member2.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
expect(member2.notifications[1].data.taskId).to.equal(groupTask[0]._id);
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
});
it('assigns a task to a user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
it('sends a notification to assigned user', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
await member.sync();
const groupTask = await user.get(`/tasks/group/${guild._id}`);
@@ -137,20 +111,27 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
});
it('assigns a task to multiple users', async () => {
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id, member2._id]);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(member1SyncedTask).to.exist;
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(groupTask[0].group.assignedUsersDetail[member2._id]).to.exist;
expect(member2SyncedTask).to.exist;
});
@@ -159,13 +140,17 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/assign`, [member._id]);
const groupTask = await member2.get(`/tasks/group/${guild._id}`);
await member.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(groupTask[0].group.assignedUsersDetail[member._id]).to.exist;
expect(syncedTask).to.exist;
});
});
@@ -37,7 +37,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
notes: 1976,
});
await user.post(`/tasks/${task._id}/assign/${member._id}`);
await user.post(`/tasks/${task._id}/assign`, [member._id]);
});
it('returns error when task is not found', async () => {
@@ -96,7 +96,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
});
it('unassigns a user and only that user from a task', async () => {
await user.post(`/tasks/${task._id}/assign/${member2._id}`);
await user.post(`/tasks/${task._id}/assign`, [member2._id]);
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
@@ -105,6 +105,9 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
const memberTasks = await member.get('/tasks/user');
const member1SyncedTask = find(memberTasks, findAssignedTask);
await member2.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
@@ -130,20 +133,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(syncedTask).to.not.exist;
});
it('allows a user to unassign themselves', async () => {
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
const groupTask = await user.get(`/tasks/group/${guild._id}`);
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
// here, but then we had a request to allow to unaissgn.
xit('returns error when non leader tries to unassign their a task', async () => {
it('returns error when non leader tries to unassign a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
@@ -1,4 +1,3 @@
import { find } from 'lodash';
import {
createAndPopulateGroup, translate as t,
} from '../../../../../helpers/api-integration/v3';
@@ -11,10 +10,6 @@ describe('PUT /tasks/:id', () => {
let habit;
let todo;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
}
beforeEach(async () => {
const { group, members, groupLeader } = await createAndPopulateGroup({
groupDetails: {
@@ -44,8 +39,7 @@ describe('PUT /tasks/:id', () => {
notes: 1976,
});
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
await user.post(`/tasks/${habit._id}/assign/${member2._id}`);
await user.post(`/tasks/${habit._id}/assign`, [member._id, member2._id]);
});
it('updates a group task', async () => {
@@ -56,28 +50,6 @@ describe('PUT /tasks/:id', () => {
expect(savedHabit.notes).to.eql('some new notes');
});
it('updates a group task - approval is required', async () => {
// allow to manage
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
// change the habit
habit = await member.put(`/tasks/${habit._id}`, {
text: 'new text!',
requiresApproval: true,
});
const memberTasks = await member2.get('/tasks/user');
const syncedTask = find(memberTasks, memberTask => memberTask.group.taskId === habit._id);
// score up to trigger approval
const response = await member2.post(`/tasks/${syncedTask._id}/score/up`);
expect(response.data.requiresApproval).to.equal(true);
expect(response.message).to.equal(t('taskApprovalHasBeenRequested'));
});
it('member updates a group task value - not allowed', async () => {
// change the todo
await expect(member.put(`/tasks/${habit._id}`, {
@@ -120,7 +92,7 @@ describe('PUT /tasks/:id', () => {
],
});
await user.post(`/tasks/${habit._id}/assign/${member._id}`);
await user.post(`/tasks/${habit._id}/assign`, [member._id]);
// change the checklist text
habit = await user.put(`/tasks/${habit._id}`, {
@@ -137,63 +109,4 @@ describe('PUT /tasks/:id', () => {
expect(habit.checklist.length).to.eql(2);
});
it('updates the linked tasks', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
it('updates the linked tasks for all assigned users', async () => {
await user.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
const member2Tasks = await member2.get('/tasks/user');
const member2SyncedTask = find(member2Tasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
expect(member2SyncedTask.text).to.eql('some new text');
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${habit._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
const memberTasks = await member.get('/tasks/user');
const syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});
@@ -289,45 +289,6 @@ describe('DELETE /user', () => {
});
});
context('user with Facebook auth', async () => {
beforeEach(async () => {
user = await generateUser({
auth: {
facebook: {
id: 'facebook-id',
},
},
});
});
it('returns an error if confirmation phrase is wrong', async () => {
await expect(user.del('/user', {
password: 'just-do-it',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase', { magicWord: 'DELETE' }),
});
});
it('returns an error if confirmation phrase is not supplied', async () => {
await expect(user.del('/user', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('deletes a Facebook user', async () => {
await user.del('/user', {
password: DELETE_CONFIRMATION,
});
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
});
});
context('user with Google auth', async () => {
beforeEach(async () => {
user = await generateUser({
@@ -145,19 +145,16 @@ describe('POST /user/class/cast/:spellId', () => {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
const memberTasks = await groupLeader.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
@@ -279,7 +276,10 @@ describe('POST /user/class/cast/:spellId', () => {
type: 'todo',
});
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post('/user/class/cast/brightness');
await user.sync();
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.sync();
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await user.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
@@ -35,7 +35,7 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
message: 'Tag list must be an array.',
});
});
@@ -20,44 +20,6 @@ describe('DELETE social registration', () => {
});
});
context('Facebook', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.local': { ok: true },
});
await expect(user.del('/user/auth/social/facebook')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantDetachSocial'),
});
});
it('succeeds if user has a local registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
});
const response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.undefined;
});
it('succeeds if user has a google registration', async () => {
await user.update({
'auth.facebook.id': 'some-fb-id',
'auth.google.id': 'some-google-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/facebook');
expect(response).to.eql({});
await user.sync();
expect(user.auth.facebook).to.be.undefined;
});
});
context('Google', () => {
it('fails if user does not have an alternative registration method', async () => {
await user.update({
@@ -81,19 +43,6 @@ describe('DELETE social registration', () => {
await user.sync();
expect(user.auth.google).to.be.undefined;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.google.id': 'some-google-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/google');
expect(response).to.eql({});
await user.sync();
expect(user.auth.goodl).to.be.undefined;
});
});
context('Apple', () => {
@@ -119,18 +68,5 @@ describe('DELETE social registration', () => {
await user.sync();
expect(user.auth.apple).to.be.undefined;
});
it('succeeds if user has a facebook registration', async () => {
await user.update({
'auth.apple.id': 'some-apple-id',
'auth.facebook.id': 'some-facebook-id',
'auth.local': { ok: true },
});
const response = await user.del('/user/auth/social/apple');
expect(response).to.eql({});
await user.sync();
expect(user.auth.goodl).to.be.undefined;
});
});
});
@@ -12,7 +12,6 @@ describe('POST /user/auth/social', () => {
let user;
const endpoint = '/user/auth/social';
let randomAccessToken = '123456';
let randomFacebookId = 'facebookId';
let randomGoogleId = 'googleId';
let network = 'NoNetwork';
@@ -33,146 +32,6 @@ describe('POST /user/auth/social', () => {
});
});
describe('facebook', () => {
beforeEach(async () => {
randomFacebookId = generateUUID();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: `${user.auth.local.username}+facebook@example.com` },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
network = 'facebook';
});
afterEach(async () => {
passport._strategies.facebook.userProfile.restore();
});
it('registers a new user', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.exist;
expect(response.id).to.exist;
expect(response.newUser).to.be.true;
expect(response.username).to.exist;
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a facebook user');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.exist;
await expect(getProperty('users', response.id, 'auth.local.email')).to.eventually.equal(`${user.auth.local.username}+facebook@example.com`);
await expect(getProperty('users', response.id, 'auth.facebook.id')).to.eventually.equal(randomFacebookId);
});
it('logs an existing user in', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.newUser).to.be.false;
expect(registerResponse.newUser).to.be.true;
});
it('logs an existing user in if they have local auth with matching email', async () => {
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('logs an existing user into their social account if they have local auth with matching email', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
// This is important for existing accounts before the new social handling
passport._strategies.facebook.userProfile.restore();
const expectedResult = {
id: randomFacebookId,
displayName: 'a facebook user',
emails: [
{ value: user.auth.local.email },
],
};
sandbox.stub(passport._strategies.facebook, 'userProfile').yields(null, expectedResult);
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(registerResponse.apiToken);
expect(response.id).to.eql(registerResponse.id);
expect(response.apiToken).not.to.eql(user.apiToken);
expect(response.id).not.to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('add social auth to an existing user', async () => {
const response = await user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(response.apiToken).to.eql(user.apiToken);
expect(response.id).to.eql(user._id);
expect(response.newUser).to.be.false;
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
expect(registerResponse.newUser).to.be.true;
await expect(user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('socialAlreadyExists'),
});
});
xit('enrolls a new user in an A/B test', async () => {
await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
await expect(getProperty('users', user._id, '_ABtests')).to.eventually.be.a('object');
});
});
describe('google', () => {
beforeEach(async () => {
randomGoogleId = generateUUID();
@@ -25,6 +25,19 @@ describe('POST /user/reset-password', async () => {
expect(user.auth.local.hashed_password).to.not.eql(previousPassword);
});
it('resets password for social users', async () => {
const email = `${user.auth.local.username}+google@example.com`;
await user.update({ 'auth.google.emails': [{ value: email }] });
await user.sync();
const previousPassword = user.auth.local.passwordResetCode;
const response = await user.post(endpoint, {
email,
});
expect(response).to.eql({ data: {}, message: t('passwordReset') });
await user.sync();
expect(user.auth.local.passwordResetCode).to.not.eql(previousPassword);
});
it('same message on error as on success', async () => {
const response = await user.post(endpoint, {
email: 'nonExistent@email.com',
@@ -130,19 +130,16 @@ describe('POST /user/class/cast/:spellId', () => {
text: 'todo group',
type: 'todo',
});
await groupLeader.post(`/tasks/${groupTask._id}/assign/${groupLeader._id}`);
const memberTasks = await groupLeader.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === group._id);
await groupLeader.post(`/tasks/${groupTask._id}/assign`, [groupLeader._id]);
await groupLeader.update({ 'stats.class': 'rogue', 'stats.lvl': 11 });
await sleep(0.5);
await groupLeader.sync();
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${syncedGroupTask._id}`))
await expect(groupLeader.post(`/user/class/cast/pickPocket?targetId=${groupTask._id}`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('groupTasksNoCast'),
code: 404,
error: 'NotFound',
message: t('messageTaskNotFound'),
});
});
@@ -247,7 +244,10 @@ describe('POST /user/class/cast/:spellId', () => {
type: 'todo',
});
await user.update({ 'stats.class': 'healer', 'stats.mp': 200, 'stats.lvl': 15 });
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
await user.post('/user/class/cast/brightness');
await user.sync();
+4 -1
View File
@@ -100,11 +100,14 @@ describe('POST /user/reset', () => {
text: 'todo group',
type: 'todo',
});
await user.post(`/tasks/${groupTask._id}/assign/${user._id}`);
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.sync();
await user.put('/user', {
'preferences.tasks.mirrorGroupTasks': [guild._id],
});
const memberTasks = await user.get('/tasks/user');
const syncedGroupTask = find(memberTasks, memberTask => memberTask.group.id === guild._id);
+1 -1
View File
@@ -34,7 +34,7 @@ describe('PUT /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'mustBeArray',
message: 'Tag list must be an array.',
});
});
+14 -5
View File
@@ -213,7 +213,7 @@ describe('cron utility functions', () => {
};
}
it('offset 0, next date in 3 months', () => {
it('monthly plan, next date in 3 months', () => {
const user = baseUserData(60, 0, 'group_plan_auto');
const planContext = getPlanContext(user, now);
@@ -222,8 +222,8 @@ describe('cron utility functions', () => {
.to.be.sameMoment('2022-08-10T02:00:00.144Z');
});
it('offset 1, next date in 1 months', () => {
const user = baseUserData(60, 1, 'group_plan_auto');
it('monthly plan, next date in 1 month', () => {
const user = baseUserData(62, 0, 'group_plan_auto');
const planContext = getPlanContext(user, now);
@@ -231,8 +231,17 @@ describe('cron utility functions', () => {
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
});
it('offset 2, next date in 2 months - with any plan', () => {
const user = baseUserData(60, 2, 'basic_3mo');
it('multi-month plan, no offset', () => {
const user = baseUserData(60, 0, 'basic_3mo');
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-06-10T02:00:00.144Z');
});
it('multi-month plan with offset', () => {
const user = baseUserData(60, 1, 'basic_3mo');
const planContext = getPlanContext(user, now);
-12
View File
@@ -249,18 +249,6 @@ describe('shared.ops.scoreTask', () => {
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
});
it('does not modify stats when task need approval', () => {
todo.group.approval.required = true;
options = {
user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false,
};
scoreTask(options);
expect(ref.afterUser.stats.hp).to.eql(50);
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
});
context('habits', () => {
it('up', () => {
options = {
+168 -69
View File
@@ -30,9 +30,9 @@
}
},
"@amplitude/types": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.0.tgz",
"integrity": "sha512-xN0gnhutztv6kqHaZ2bre18anQV5GDmMXOeipTvI670g2VjNbPfOzMwu1LN4p1NadYq+GqYI223UcZrXR+R4Pw=="
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
"integrity": "sha512-I8qenRI7uU6wKNb9LiZrAosSHVoNHziXouKY81CrqxH9xhVTEIJFXeuCV0hbtBr0Al/8ejnGjQRx+S2SvU/pPg=="
},
"@amplitude/ua-parser-js": {
"version": "0.7.31",
@@ -40,12 +40,19 @@
"integrity": "sha512-+z8UGRaj13Pt5NDzOnkTBy49HE2CX64jeL0ArB86HAtilpnfkPB7oqkigN7Lf2LxscMg4QhFD7mmCfedh3rqTg=="
},
"@amplitude/utils": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.0.tgz",
"integrity": "sha512-/R8j8IzFH0GYfA6ehQDm5IEzt71gIeMdiYYFIzZp6grERQlgJcwNJMAiza0o2JwwTDIruzqdB3c/vLVjuakp+w==",
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@amplitude/utils/-/utils-1.10.2.tgz",
"integrity": "sha512-tVsHXu61jITEtRjB7NugQ5cVDd4QDzne8T3ifmZye7TiJeUfVRvqe44gDtf55A+7VqhDhyEIIXTA1iVcDGqlEw==",
"requires": {
"@amplitude/types": "^1.10.0",
"tslib": "^1.9.3"
"@amplitude/types": "^1.10.2",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
}
}
},
"@babel/code-frame": {
@@ -1799,26 +1806,26 @@
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.6.tgz",
"integrity": "sha512-PatI6elL5eMzoypFAiYDpYQyMtXTn+iMhuxxQt5mAXD4fEmKorpSI3PHd+i3JXBJN3xyA6MvJv7at23HffFHwA==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz",
"integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.6",
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.9",
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.9",
"@babel/plugin-syntax-optional-chaining": "^7.8.3"
},
"dependencies": {
"@babel/helper-plugin-utils": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz",
"integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg=="
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz",
"integrity": "sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w=="
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.6.tgz",
"integrity": "sha512-4KoLhwGS9vGethZpAhYnMejWkX64wsnHPDwvOsKWU6Fg4+AlK2Jz3TyjQLMEPvz+1zemi/WBdkYxCD0bAfIkiw==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.18.9.tgz",
"integrity": "sha512-imytd2gHi3cJPsybLRbmFrF7u5BIEuI2cNheyKi3/iOBC63kNn3q8Crn2xVuESli0aM4KYsyEqKyS7lFL8YVtw==",
"requires": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.18.9"
}
},
"@babel/helper-validator-identifier": {
@@ -1827,9 +1834,9 @@
"integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g=="
},
"@babel/types": {
"version": "7.18.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.7.tgz",
"integrity": "sha512-QG3yxTcTIBoAcQmkCs+wAPYZhu7Dk9rXKacINfNbdJDNERTbLQbHGyVG8q/YGMPeCJRIhSY0+fTc5+xuh6WPSQ==",
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.9.tgz",
"integrity": "sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg==",
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"to-fast-properties": "^2.0.0"
@@ -4382,6 +4389,49 @@
"postcss": "^7.0.0"
}
},
"@jridgewell/gen-mapping": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
"integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
"requires": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/resolve-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
"integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w=="
},
"@jridgewell/set-array": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
},
"@jridgewell/source-map": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
"integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
"requires": {
"@jridgewell/gen-mapping": "^0.3.0",
"@jridgewell/trace-mapping": "^0.3.9"
}
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"@mdx-js/mdx": {
"version": "1.6.22",
"resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-1.6.22.tgz",
@@ -6446,6 +6496,11 @@
"@types/react": "*"
}
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -7148,20 +7203,14 @@
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"terser-webpack-plugin": {
@@ -9405,6 +9454,11 @@
"@types/react": "*"
}
},
"acorn": {
"version": "8.7.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A=="
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -9947,20 +10001,14 @@
}
},
"terser": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.10.0.tgz",
"integrity": "sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==",
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"requires": {
"@jridgewell/source-map": "^0.3.2",
"acorn": "^8.5.0",
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.20"
},
"dependencies": {
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="
}
}
},
"terser-webpack-plugin": {
@@ -13086,13 +13134,13 @@
"integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM="
},
"amplitude-js": {
"version": "8.18.5",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.18.5.tgz",
"integrity": "sha512-s43q4qKd7kvhYESQhYvyKDKUM1PpyAyoOFFlyMuFfQHRxyeDmZRhcfzrKnOhbrLhFxSWtPc0VEeh9tajJRNe5Q==",
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.0.tgz",
"integrity": "sha512-kC01TmmCdDrtms8LhvC/r65FtbmCbNHZ1/jiezXmTH82TsWI/SkN47jKs8CCwjZNakqTBN/hmficiZBUKv4myw==",
"requires": {
"@amplitude/analytics-connector": "1.4.4",
"@amplitude/ua-parser-js": "0.7.31",
"@amplitude/utils": "^1.0.5",
"@amplitude/utils": "^1.10.1",
"@babel/runtime": "^7.3.4",
"blueimp-md5": "^2.10.0",
"query-string": "5"
@@ -15839,9 +15887,9 @@
}
},
"core-js": {
"version": "3.23.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.23.5.tgz",
"integrity": "sha512-7Vh11tujtAZy82da4duVreQysIoO2EvVrur7y6IzZkH1IHPSekuDi8Vuw1+YKjkbfWLRD7Nc9ICQ/sIUDutcyg=="
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
"integrity": "sha512-0QTBSYSUZ6Gq21utGzkfITDylE8jWC9Ne1D2MrhvlsZBI1x39OdDIVbzSqtgMndIy6BlHxBXpMGqzZmnztg2rg=="
},
"core-js-compat": {
"version": "3.11.0",
@@ -16293,7 +16341,7 @@
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
},
"debug": {
"version": "4.1.1",
@@ -16771,9 +16819,9 @@
}
},
"dompurify": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz",
"integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
"integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
},
"domutils": {
"version": "1.7.0",
@@ -20627,9 +20675,9 @@
}
},
"jquery": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz",
"integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw=="
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.1.tgz",
"integrity": "sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw=="
},
"js-message": {
"version": "1.0.5",
@@ -27297,9 +27345,9 @@
"integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg=="
},
"terser": {
"version": "4.6.7",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.6.7.tgz",
"integrity": "sha512-fmr7M1f7DBly5cX2+rFDvmGBAaaZyPrHYK4mMdHEDAdNTqXSZgSOfqsfGq2HqPGT/1V0foZZuCZFx8CHKgAk3g==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
"requires": {
"commander": "^2.20.0",
"source-map": "~0.6.1",
@@ -28265,9 +28313,60 @@
"integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk="
},
"vue": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz",
"integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ=="
"version": "2.7.10",
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.10.tgz",
"integrity": "sha512-HmFC70qarSHPXcKtW8U8fgIkF6JGvjEmDiVInTkKZP0gIlEPhlVlcJJLkdGIDiNkIeA2zJPQTWJUI4iWe+AVfg==",
"requires": {
"@vue/compiler-sfc": "2.7.10",
"csstype": "^3.1.0"
},
"dependencies": {
"@babel/parser": {
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg=="
},
"@vue/compiler-sfc": {
"version": "2.7.10",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.10.tgz",
"integrity": "sha512-55Shns6WPxlYsz4WX7q9ZJBL77sKE1ZAYNYStLs6GbhIOMrNtjMvzcob6gu3cGlfpCR4bT7NXgyJ3tly2+Hx8Q==",
"requires": {
"@babel/parser": "^7.18.4",
"postcss": "^8.4.14",
"source-map": "^0.6.1"
}
},
"csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA=="
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
"integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
},
"postcss": {
"version": "8.4.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
"requires": {
"nanoid": "^3.3.4",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
}
}
},
"vue-cli-plugin-storybook": {
"version": "2.1.0",
@@ -28584,12 +28683,12 @@
}
},
"vue-template-compiler": {
"version": "2.6.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz",
"integrity": "sha512-ODQS1SyMbjKoO1JBJZojSw6FE4qnh9rIpUZn2EUT86FKizx9uH5z6uXiIrm4/Nb/gwxTi/o17ZDEGWAXHvtC7g==",
"version": "2.7.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz",
"integrity": "sha512-QO+8R9YRq1Gudm8ZMdo/lImZLJVUIAM8c07Vp84ojdDAf8HmPJc7XB556PcXV218k2AkKznsRz6xB5uOjAC4EQ==",
"requires": {
"de-indent": "^1.0.2",
"he": "^1.1.0"
"he": "^1.2.0"
}
},
"vue-template-es2015-compiler": {
+7 -7
View File
@@ -25,15 +25,15 @@
"@vue/cli-plugin-unit-mocha": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/test-utils": "1.0.0-beta.29",
"amplitude-js": "^8.18.5",
"amplitude-js": "^8.21.0",
"axios": "^0.25.0",
"axios-progress-bar": "^1.2.0",
"babel-eslint": "^10.1.0",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.22.0",
"chai": "^4.3.6",
"core-js": "^3.23.5",
"dompurify": "^2.3.8",
"core-js": "^3.24.1",
"dompurify": "^2.4.0",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
@@ -42,7 +42,7 @@
"hellojs": "^1.19.5",
"inspectpack": "^4.7.1",
"intro.js": "^5.1.0",
"jquery": "^3.6.0",
"jquery": "^3.6.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nconf": "^0.12.0",
@@ -55,16 +55,16 @@
"svgo-loader": "^2.2.1",
"uuid": "^8.3.2",
"validator": "^13.7.0",
"vue": "^2.6.14",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.5.4",
"vue-template-compiler": "^2.6.14",
"vue-template-compiler": "^2.7.10",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^4.46.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.18.6"
"@babel/plugin-proposal-optional-chaining": "^7.18.9"
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ if (process.env.NODE_ENV === 'production') {
stdio: 'inherit',
});
execSync('npm run storybook:build', {
/* execSync('npm run storybook:build', {
stdio: 'inherit',
});
}); */
}
@@ -463,6 +463,11 @@
width: 48px;
height: 52px;
}
.achievement-woodlandWizard2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-woodlandWizard2x.png');
width: 60px;
height: 64px;
}
.achievement-zodiac2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-zodiac2x.png');
width: 60px;
@@ -565,6 +570,11 @@
width: 141px;
height: 147px;
}
.background_autumn_picnic {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_picnic.png');
width: 141px;
height: 147px;
}
.background_autumn_poplars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_autumn_poplars.png');
width: 141px;
@@ -690,6 +700,11 @@
width: 141px;
height: 147px;
}
.background_by_a_campfire {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_by_a_campfire.png');
width: 141px;
height: 147px;
}
.background_camping_out {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_camping_out.png');
width: 141px;
@@ -1354,6 +1369,11 @@
width: 141px;
height: 147px;
}
.background_messy_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_messy_room.png');
width: 141px;
height: 147px;
}
.background_meteor_shower {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_meteor_shower.png');
width: 141px;
@@ -1424,6 +1444,11 @@
width: 141px;
height: 147px;
}
.background_old_photo {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_old_photo.png');
width: 141px;
height: 147px;
}
.background_on_a_castle_wall {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_castle_wall.png');
width: 141px;
@@ -1509,6 +1534,11 @@
width: 141px;
height: 147px;
}
.background_rainbow_eucalyptus {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_eucalyptus.png');
width: 141px;
height: 147px;
}
.background_rainbow_meadow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rainbow_meadow.png');
width: 141px;
@@ -1819,6 +1849,11 @@
width: 141px;
height: 147px;
}
.background_theatre_stage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_theatre_stage.png');
width: 141px;
height: 147px;
}
.background_throne_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_throne_room.png');
width: 141px;
@@ -2106,6 +2141,11 @@
width: 68px;
height: 68px;
}
.icon_background_autumn_picnic {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_autumn_picnic.png');
width: 68px;
height: 68px;
}
.icon_background_autumn_poplars {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_autumn_poplars.png');
width: 68px;
@@ -2231,6 +2271,11 @@
width: 68px;
height: 68px;
}
.icon_background_by_a_campfire {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_by_a_campfire.png');
width: 68px;
height: 68px;
}
.icon_background_camping_out {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_camping_out.png');
width: 68px;
@@ -2900,6 +2945,11 @@
width: 68px;
height: 68px;
}
.icon_background_messy_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_messy_room.png');
width: 68px;
height: 68px;
}
.icon_background_meteor_shower {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_meteor_shower.png');
width: 68px;
@@ -2970,6 +3020,11 @@
width: 68px;
height: 68px;
}
.icon_background_old_photo {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_old_photo.png');
width: 68px;
height: 68px;
}
.icon_background_on_a_castle_wall {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_on_a_castle_wall.png');
width: 68px;
@@ -3055,6 +3110,11 @@
width: 68px;
height: 68px;
}
.icon_background_rainbow_eucalyptus {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_eucalyptus.png');
width: 68px;
height: 68px;
}
.icon_background_rainbow_meadow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_rainbow_meadow.png');
width: 68px;
@@ -3370,6 +3430,11 @@
width: 68px;
height: 68px;
}
.icon_background_theatre_stage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_theatre_stage.png');
width: 68px;
height: 68px;
}
.icon_background_throne_room {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_throne_room.png');
width: 68px;
@@ -18475,6 +18540,11 @@
width: 114px;
height: 87px;
}
.eyewear_armoire_comedyMask {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_comedyMask.png');
width: 114px;
height: 90px;
}
.eyewear_armoire_goofyGlasses {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_goofyGlasses.png');
width: 90px;
@@ -18485,6 +18555,11 @@
width: 90px;
height: 90px;
}
.eyewear_armoire_tragedyMask {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_armoire_tragedyMask.png');
width: 114px;
height: 90px;
}
.headAccessory_armoire_comicalArrow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_armoire_comicalArrow.png');
width: 90px;
@@ -18970,6 +19045,11 @@
width: 90px;
height: 90px;
}
.shield_armoire_dustpan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_dustpan.png');
width: 114px;
height: 90px;
}
.shield_armoire_fancyBlownGlassVase {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_fancyBlownGlassVase.png');
width: 114px;
@@ -19685,6 +19765,11 @@
width: 68px;
height: 68px;
}
.shop_eyewear_armoire_comedyMask {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_comedyMask.png');
width: 68px;
height: 68px;
}
.shop_eyewear_armoire_goofyGlasses {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_goofyGlasses.png');
width: 68px;
@@ -19695,6 +19780,11 @@
width: 68px;
height: 68px;
}
.shop_eyewear_armoire_tragedyMask {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_armoire_tragedyMask.png');
width: 68px;
height: 68px;
}
.shop_headAccessory_armoire_comicalArrow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_armoire_comicalArrow.png');
width: 68px;
@@ -20180,6 +20270,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_dustpan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_dustpan.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_fancyBlownGlassVase {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_fancyBlownGlassVase.png');
width: 68px;
@@ -20565,6 +20660,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_featherDuster {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_featherDuster.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_festivalFirecracker {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_festivalFirecracker.png');
width: 68px;
@@ -20805,6 +20905,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_pushBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_pushBroom.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_rancherLasso {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_rancherLasso.png');
width: 68px;
@@ -21430,6 +21535,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_featherDuster {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_featherDuster.png');
width: 114px;
height: 90px;
}
.weapon_armoire_festivalFirecracker {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_festivalFirecracker.png');
width: 90px;
@@ -21670,6 +21780,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_pushBroom {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pushBroom.png');
width: 114px;
height: 90px;
}
.weapon_armoire_rancherLasso {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_rancherLasso.png');
width: 90px;
@@ -22760,6 +22875,26 @@
width: 114px;
height: 90px;
}
.broad_armor_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Healer.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Mage.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Rogue.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fall2022Warrior.png');
width: 114px;
height: 90px;
}
.broad_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_fallHealer.png');
width: 90px;
@@ -22930,6 +23065,26 @@
width: 114px;
height: 90px;
}
.head_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Healer.png');
width: 114px;
height: 90px;
}
.head_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Mage.png');
width: 114px;
height: 90px;
}
.head_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Rogue.png');
width: 114px;
height: 90px;
}
.head_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fall2022Warrior.png');
width: 114px;
height: 90px;
}
.head_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_fallHealer.png');
width: 90px;
@@ -23055,6 +23210,21 @@
width: 114px;
height: 90px;
}
.shield_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Healer.png');
width: 114px;
height: 90px;
}
.shield_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Rogue.png');
width: 114px;
height: 90px;
}
.shield_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fall2022Warrior.png');
width: 114px;
height: 90px;
}
.shield_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_fallHealer.png');
width: 90px;
@@ -23210,6 +23380,26 @@
width: 68px;
height: 68px;
}
.shop_armor_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Healer.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Mage.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Rogue.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fall2022Warrior.png');
width: 68px;
height: 68px;
}
.shop_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_special_fallHealer.png');
width: 68px;
@@ -23380,6 +23570,26 @@
width: 68px;
height: 68px;
}
.shop_head_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Healer.png');
width: 68px;
height: 68px;
}
.shop_head_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Mage.png');
width: 68px;
height: 68px;
}
.shop_head_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Rogue.png');
width: 68px;
height: 68px;
}
.shop_head_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fall2022Warrior.png');
width: 68px;
height: 68px;
}
.shop_head_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_special_fallHealer.png');
width: 68px;
@@ -23505,6 +23715,21 @@
width: 68px;
height: 68px;
}
.shop_shield_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Healer.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Rogue.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fall2022Warrior.png');
width: 68px;
height: 68px;
}
.shop_shield_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_special_fallHealer.png');
width: 68px;
@@ -23660,6 +23885,26 @@
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Healer.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Mage.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Rogue.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fall2022Warrior.png');
width: 68px;
height: 68px;
}
.shop_weapon_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_special_fallHealer.png');
width: 68px;
@@ -23820,6 +24065,26 @@
width: 114px;
height: 90px;
}
.slim_armor_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Healer.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Mage.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Rogue.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fall2022Warrior.png');
width: 114px;
height: 90px;
}
.slim_armor_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_fallHealer.png');
width: 90px;
@@ -23980,6 +24245,26 @@
width: 114px;
height: 90px;
}
.weapon_special_fall2022Healer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Healer.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2022Mage {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Mage.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2022Rogue {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Rogue.png');
width: 114px;
height: 90px;
}
.weapon_special_fall2022Warrior {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fall2022Warrior.png');
width: 114px;
height: 90px;
}
.weapon_special_fallHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_fallHealer.png');
width: 90px;
@@ -26925,6 +27210,86 @@
width: 117px;
height: 120px;
}
.eyewear_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202208.png');
width: 114px;
height: 90px;
}
.head_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202208.png');
width: 114px;
height: 90px;
}
.shop_eyewear_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_eyewear_mystery_202208.png');
width: 68px;
height: 68px;
}
.shop_head_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202208.png');
width: 68px;
height: 68px;
}
.shop_set_mystery_202208 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202208.png');
width: 68px;
height: 68px;
}
.shield_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202209.png');
width: 114px;
height: 90px;
}
.shop_set_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202209.png');
width: 68px;
height: 68px;
}
.shop_shield_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_mystery_202209.png');
width: 68px;
height: 68px;
}
.shop_weapon_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_mystery_202209.png');
width: 68px;
height: 68px;
}
.weapon_mystery_202209 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202209.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202210.png');
width: 117px;
height: 120px;
}
.head_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202210.png');
width: 117px;
height: 120px;
}
.shop_armor_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202210.png');
width: 68px;
height: 68px;
}
.shop_head_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_mystery_202210.png');
width: 68px;
height: 68px;
}
.shop_set_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202210.png');
width: 68px;
height: 68px;
}
.slim_armor_mystery_202210 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202210.png');
width: 117px;
height: 120px;
}
.broad_armor_mystery_301404 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
width: 90px;
@@ -36603,6 +36968,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_BearCub-Rainbow.png');
width: 105px;
@@ -37018,6 +37388,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Cactus-Rainbow.png');
width: 105px;
@@ -37533,6 +37908,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Rainbow.png');
width: 105px;
@@ -37948,6 +38328,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_FlyingPig-Rainbow.png');
width: 105px;
@@ -38213,6 +38598,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Fox-Rainbow.png');
width: 105px;
@@ -38868,6 +39258,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_LionCub-Rainbow.png');
width: 105px;
@@ -39353,6 +39748,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_PandaCub-Rainbow.png');
width: 105px;
@@ -40573,6 +40973,11 @@
width: 105px;
height: 105px;
}
.Mount_Body_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Body_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_TigerCub-Rainbow.png');
width: 105px;
@@ -41148,6 +41553,11 @@
width: 135px;
height: 135px;
}
.Mount_Body_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Porcelain.png');
width: 135px;
height: 135px;
}
.Mount_Body_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Wolf-Rainbow.png');
width: 135px;
@@ -41668,6 +42078,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_BearCub-Rainbow.png');
width: 105px;
@@ -42083,6 +42498,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Cactus-Rainbow.png');
width: 105px;
@@ -42598,6 +43018,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Rainbow.png');
width: 105px;
@@ -43013,6 +43438,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_FlyingPig-Rainbow.png');
width: 105px;
@@ -43278,6 +43708,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Fox-Rainbow.png');
width: 105px;
@@ -43933,6 +44368,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_LionCub-Rainbow.png');
width: 105px;
@@ -44418,6 +44858,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_PandaCub-Rainbow.png');
width: 105px;
@@ -45638,6 +46083,11 @@
width: 105px;
height: 105px;
}
.Mount_Head_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Porcelain.png');
width: 105px;
height: 105px;
}
.Mount_Head_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_TigerCub-Rainbow.png');
width: 105px;
@@ -46213,6 +46663,11 @@
width: 135px;
height: 135px;
}
.Mount_Head_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Porcelain.png');
width: 135px;
height: 135px;
}
.Mount_Head_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Wolf-Rainbow.png');
width: 135px;
@@ -46738,6 +47193,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_BearCub-Rainbow.png');
width: 81px;
@@ -47153,6 +47613,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Cactus-Rainbow.png');
width: 81px;
@@ -47668,6 +48133,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Dragon-Rainbow.png');
width: 81px;
@@ -48083,6 +48553,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_FlyingPig-Rainbow.png');
width: 81px;
@@ -48348,6 +48823,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Fox-Rainbow.png');
width: 81px;
@@ -49008,6 +49488,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_LionCub-Rainbow.png');
width: 81px;
@@ -49493,6 +49978,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_PandaCub-Rainbow.png');
width: 81px;
@@ -50713,6 +51203,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_TigerCub-Rainbow.png');
width: 81px;
@@ -51288,6 +51783,11 @@
width: 81px;
height: 99px;
}
.Mount_Icon_Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Porcelain.png');
width: 81px;
height: 99px;
}
.Mount_Icon_Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Icon_Wolf-Rainbow.png');
width: 81px;
@@ -51818,6 +52318,11 @@
width: 81px;
height: 99px;
}
.Pet-BearCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-BearCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Rainbow.png');
width: 81px;
@@ -52248,6 +52753,11 @@
width: 81px;
height: 99px;
}
.Pet-Cactus-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Cactus-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Rainbow.png');
width: 81px;
@@ -52783,6 +53293,11 @@
width: 81px;
height: 99px;
}
.Pet-Dragon-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Dragon-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Rainbow.png');
width: 81px;
@@ -53213,6 +53728,11 @@
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-FlyingPig-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Rainbow.png');
width: 81px;
@@ -53493,6 +54013,11 @@
width: 81px;
height: 99px;
}
.Pet-Fox-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Fox-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Rainbow.png');
width: 81px;
@@ -54168,6 +54693,11 @@
width: 81px;
height: 99px;
}
.Pet-LionCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-LionCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Rainbow.png');
width: 81px;
@@ -54668,6 +55198,11 @@
width: 81px;
height: 99px;
}
.Pet-PandaCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-PandaCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Rainbow.png');
width: 81px;
@@ -55913,6 +56448,11 @@
width: 81px;
height: 99px;
}
.Pet-TigerCub-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-TigerCub-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Rainbow.png');
width: 81px;
@@ -56508,6 +57048,11 @@
width: 81px;
height: 99px;
}
.Pet-Wolf-Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Porcelain.png');
width: 81px;
height: 99px;
}
.Pet-Wolf-Rainbow {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Rainbow.png');
width: 81px;
@@ -56838,6 +57383,11 @@
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Porcelain {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Porcelain.png');
width: 68px;
height: 68px;
}
.Pet_HatchingPotion_Purple {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Purple.png');
width: 68px;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

+53 -62
View File
@@ -1,9 +1,56 @@
.create-task-area {
position: absolute;
right: 24px;
right: 12px;
top: -24px;
z-index: 998;
align-items: center;
.dropdown {
width: 140px;
border-radius: 2px;
box-shadow: 0 3px 6px 0 rgba(26, 24, 29, 0.16), 0 3px 6px 0 rgba(26, 24, 29, 0.24);
background-color: $white;
margin-top: 2px;
position: absolute;
right: 0px;
.task-icon {
width: 30px;
}
.task-label {
margin-top: 1px;
}
.dropdown-item:hover {
.svg-icon {
color: $purple-300;
}
}
.svg-icon {
color: $gray-200;
&.icon-habit {
width: 30px;
height: 20px;
}
&.icon-daily {
width: 24px;
height: 20px;
}
&.icon-todo {
width: 20px;
height: 20px;
}
&.icon-reward {
width: 26px;
height: 20px;
}
}
}
}
.slide-tasks-btns-leave-active, .slide-tasks-btns-enter-active {
@@ -17,70 +64,14 @@
overflow-x: hidden;
}
.diamond-btn {
margin-left: 24px;
background: $white;
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
cursor: pointer;
color: $gray-200;
transform: rotate(45deg);
&:hover:not(.create-btn) {
color: $purple-400;
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
}
.svg-icon {
width: 20px;
height: 20px;
transform: rotate(-45deg);
&.icon-habit {
width: 24px;
height: 16px;
}
&.icon-daily {
width: 21.6px;
height: 18px;
}
&.icon-todo {
width: 18px;
height: 18px;
}
&.icon-reward {
width: 23.4px;
height: 18px;
}
}
}
.create-btn {
color: $white;
background-color: $green-100;
height: 48px;
width: 48px;
background-color: $purple-200;
height: 32px;
.svg-icon {
width: 16px;
height: 16px;
transform: rotate(-45deg);
transition: transform 0.3s cubic-bezier(0, 1, 0.5, 1);
}
&.open {
background: $gray-200 !important;
.svg-icon {
transform: rotate(-90deg);
}
color: $purple-500;
width: 10px;
height: 10px;
}
}
+12 -8
View File
@@ -180,8 +180,8 @@ input, textarea, input.form-control, textarea.form-control {
}
// used in checkboxes and radios
$bg-focused-active-control: #4f2993;
$bg-disabled-control: #34303a;
$bg-focused-active-control: $purple-200;
$bg-disabled-control: $gray-10;
// custom control
.custom-control {
@@ -231,17 +231,21 @@ $bg-disabled-control: #34303a;
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-label::before, &:active:not(:checked):not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:focus:checked:not(:disabled)~.custom-control-label::before, &:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
border-color: $purple-400;
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
border-color: 2 px solid $purple-400;
background-color: $purple-400;
}
&:focus:disabled~.custom-control-label::before, &:active:disabled~.custom-control-label::before {
&:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
}
+15 -15
View File
@@ -30,7 +30,7 @@
&-bg {
background: $maroon-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $maroon-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -41,7 +41,7 @@
&-modal {
&-bg { background: $maroon-100 !important; }
&-headings { color: $white; }
&-headings { color: $white !important; }
&-icon { color: $maroon-100 !important; }
&-text { color: $red-1 !important; }
&-content {
@@ -61,7 +61,7 @@
&-bg {
background: $red-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $red-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -92,7 +92,7 @@
&-bg {
background: $orange-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($orange-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $orange-100 !important; }
&-inner-habit { background: rgba($orange-1, 0.25) !important; }
@@ -123,7 +123,7 @@
&-bg {
background: $yellow-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($yellow-1, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $yellow-100 !important; }
&-inner-habit { background: rgba($yellow-1, 0.25) !important; }
@@ -154,7 +154,7 @@
&-bg {
background: $green-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $green-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -186,7 +186,7 @@
&-bg {
background: $teal-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $teal-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -217,7 +217,7 @@
&-bg {
background: $blue-100 !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive { background: $blue-100 !important; }
&-inner-habit { background: rgba($black, 0.25) !important; }
@@ -249,7 +249,7 @@
&-bg {
background: $purple-task !important;
.habit-control:not(.task-not-scoreable):hover { background: rgba($black, 0.5) !important; }
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-inner-habit { background: rgba($black, 0.25) !important; }
&-inner-daily-todo { background: rgba($white, 0.5) !important; }
@@ -258,7 +258,7 @@
&-modal {
&-bg { background: $purple-300 !important; }
&-headings { color: $white; }
&-headings { color: $white !important; }
&-icon { color: $purple-300 !important; }
&-text { color: $black !important; }
&-content {
@@ -281,11 +281,11 @@
background: rgba($yellow-100, 0.15) !important;
.small-text { color: $yellow-1 !important; }
&:hover { background: rgba($yellow-100, 0.25) !important; }
&:hover:not(.task-not-scoreable) { background: rgba($yellow-100, 0.25) !important; }
}
&-bg-noninteractive {
background: rgba($yellow-100, 0.15) !important;
.small-text { color: $yellow-1 !important; }
background-color: $gray-500 !important;
.small-text { color: $gray-100 !important; }
}
}
}
@@ -312,7 +312,7 @@
&-control {
&-bg {
background: $gray-200 !important;
.daily-todo-control:hover { background: rgba($white, 0.75) !important; }
.daily-todo-control:not(.task-not-scoreable):hover { background: rgba($white, 0.75) !important; }
}
&-bg-noninteractive {
background: $gray-200 !important;
@@ -323,7 +323,7 @@
background: $gray-600;
.task-title, .task-notes {
color: $gray-300 !important;
opacity: 0.75;
}
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="16" viewBox="0 0 8 16">
<path fill-rule="evenodd" d="M7.145 8.006H4.903V16H1.58V8.006H0V5.182h1.58V3.354C1.58 2.045 2.202 0 4.933 0l2.461.01v2.742H5.608c-.291 0-.705.145-.705.77v1.66h2.533l-.291 2.824z"/>
<svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20,0H4C1.79,0,0,1.79,0,4V20c0,2.21,1.79,4,4,4H20c2.21,0,4-1.79,4-4V4c0-2.21-1.79-4-4-4Zm-3.72,6.66h-1.26c-1.24,0-1.63,.77-1.63,1.56v1.88h2.78l-.44,2.9h-2.33v7h-3.13v-7h-2.54v-2.9h2.54v-2.21c0-2.51,1.5-3.9,3.78-3.9,1.1,0,2.24,.2,2.24,.2v2.47Z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 354 B

+2 -2
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0c2.173 0 2.445.01 3.298.048.852.04 1.433.174 1.942.372.526.205.973.478 1.418.922.444.445.717.892.922 1.418.198.509.333 1.09.372 1.942C15.99 5.555 16 5.827 16 8s-.01 2.445-.048 3.298c-.04.852-.174 1.433-.372 1.942a3.924 3.924 0 0 1-.922 1.418 3.924 3.924 0 0 1-1.418.922c-.509.198-1.09.333-1.942.372-.853.04-1.125.048-3.298.048s-2.445-.009-3.298-.048c-.852-.04-1.433-.174-1.942-.372a3.924 3.924 0 0 1-1.418-.922A3.924 3.924 0 0 1 .42 13.24c-.198-.509-.333-1.09-.372-1.942C.01 10.445 0 10.173 0 8s.01-2.445.048-3.298C.088 3.85.222 3.269.42 2.76c.205-.526.478-.973.922-1.418A3.924 3.924 0 0 1 2.76.42C3.269.222 3.85.087 4.702.048 5.555.01 5.827 0 8 0zm0 3.892a4.108 4.108 0 1 0 0 8.216 4.108 4.108 0 0 0 0-8.216zm5.23-.162a.96.96 0 1 0-1.92 0 .96.96 0 0 0 1.92 0zM8 10.667a2.666 2.666 0 1 1 0-5.333 2.666 2.666 0 0 1 0 5.333z"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M14.67,12A2.67,2.67,0,1,1,12,9.33,2.67,2.67,0,0,1,14.67,12Zm3.89,0c0,1.16.1,3.66-.32,4.71a2.7,2.7,0,0,1-1.52,1.52c-1.06.42-3.55.33-4.72.33s-3.66.09-4.71-.33a2.65,2.65,0,0,1-1.52-1.52c-.42-1.05-.33-3.55-.33-4.71s-.09-3.67.33-4.72A2.65,2.65,0,0,1,7.29,5.76c1-.42,3.55-.32,4.71-.32a18,18,0,0,1,4.72.32,2.7,2.7,0,0,1,1.52,1.52C18.66,8.34,18.56,10.83,18.56,12ZM16.1,12A4.1,4.1,0,1,0,12,16.1,4.09,4.09,0,0,0,16.1,12Zm1.13-4.27a1,1,0,1,0-1,1A1,1,0,0,0,17.23,7.73ZM24,4V20a4,4,0,0,1-4,4H4a4,4,0,0,1-4-4V4A4,4,0,0,1,4,0H20A4,4,0,0,1,24,4ZM19.94,8.7a4.71,4.71,0,0,0-1.29-3.35A4.71,4.71,0,0,0,15.3,4.06C14,4,10,4,8.7,4.06A4.76,4.76,0,0,0,5.35,5.34,4.75,4.75,0,0,0,4.06,8.7C4,10,4,14,4.06,15.3a4.73,4.73,0,0,0,1.29,3.35A4.77,4.77,0,0,0,8.7,19.94c1.32.08,5.28.08,6.6,0a4.71,4.71,0,0,0,3.35-1.29,4.73,4.73,0,0,0,1.29-3.35C20,14,20,10,19.94,8.7Z" />
</svg>

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 914 B

@@ -0,0 +1,3 @@
<svg width="16" height="14" viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg">
<path d="m12.707 5.707-1.414-1.414L8 7.586 6.707 6.293 5.293 7.707 8 10.414l4.707-4.707zM16 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2h2v2h10V2H4v2.5h2l-3 3-3-3h2V2a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" fill="#878190" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 325 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM16.68,19a4.47,4.47,0,0,1-3,1C9.86,20,9,17.22,9,15.61v-4.5H7.56a.31.31,0,0,1-.31-.32V8.67a.52.52,0,0,1,.35-.5,4,4,0,0,0,2.63-3.66c0-.34.21-.51.51-.51H13a.31.31,0,0,1,.32.31v3.6h2.59a.31.31,0,0,1,.31.31v2.55a.31.31,0,0,1-.31.32H13.25v4.16c0,1.07.74,1.67,2.13,1.12a.67.67,0,0,1,.4-.07.36.36,0,0,1,.23.25l.68,2C16.75,18.71,16.8,18.89,16.68,19Z" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

+2 -2
View File
@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="13" viewBox="0 0 16 13">
<path fill-rule="evenodd" d="M14.362 3.238c.007.141.01.281.01.424 0 4.338-3.302 9.34-9.34 9.34A9.284 9.284 0 0 1 0 11.527c.257.029.518.045.783.045a6.576 6.576 0 0 0 4.076-1.404 3.288 3.288 0 0 1-3.065-2.28 3.312 3.312 0 0 0 1.481-.056A3.288 3.288 0 0 1 .642 4.613v-.041c.444.246.949.393 1.488.41A3.28 3.28 0 0 1 .67 2.25c0-.602.162-1.166.444-1.651a9.315 9.315 0 0 0 6.766 3.43A3.28 3.28 0 0 1 11.078 0c.943 0 1.797.398 2.395 1.035a6.565 6.565 0 0 0 2.085-.797 3.289 3.289 0 0 1-1.443 1.816A6.543 6.543 0 0 0 16 1.539a6.665 6.665 0 0 1-1.638 1.699"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M20,0H4A4,4,0,0,0,0,4V20a4,4,0,0,0,4,4H20a4,4,0,0,0,4-4V4A4,4,0,0,0,20,0ZM18.36,8.74c0,.14,0,.29,0,.43A9.34,9.34,0,0,1,4,17a6.85,6.85,0,0,0,.79,0,6.57,6.57,0,0,0,4.07-1.4A3.29,3.29,0,0,1,5.8,13.39a4.1,4.1,0,0,0,.62,0,3.49,3.49,0,0,0,.86-.11,3.28,3.28,0,0,1-2.63-3.22v0a3.35,3.35,0,0,0,1.48.42A3.29,3.29,0,0,1,4.67,7.76,3.22,3.22,0,0,1,5.12,6.1a9.3,9.3,0,0,0,6.76,3.43,3.67,3.67,0,0,1-.08-.75,3.28,3.28,0,0,1,5.67-2.24,6.54,6.54,0,0,0,2.08-.79,3.22,3.22,0,0,1-1.44,1.8A6.67,6.67,0,0,0,20,7.05,7.31,7.31,0,0,1,18.36,8.74Z" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 622 B

+1 -20
View File
@@ -1,20 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="rt3mruthma" d="M11.2 13.2c0-1.231-.467-2.35-1.23-3.2h1.23c1.765 0 3.2 1.435 3.2 3.2h-3.2zm-9.6 0c0-1.765 1.435-3.2 3.2-3.2h1.6c1.765 0 3.2 1.435 3.2 3.2h-8zm4-9.6C6.926 3.6 8 4.674 8 6c0 1.326-1.074 2.4-2.4 2.4-1.326 0-2.4-1.074-2.4-2.4 0-1.326 1.074-2.4 2.4-2.4zm3.452.415C9.436 3.754 9.9 3.6 10.4 3.6c1.326 0 2.4 1.074 2.4 2.4 0 1.326-1.074 2.4-2.4 2.4-.5 0-.964-.154-1.348-.415.34-.587.548-1.26.548-1.985 0-.726-.209-1.398-.548-1.985zm4.154 4.829C13.942 8.118 14.4 7.112 14.4 6c0-2.206-1.794-4-4-4-.904 0-1.73.313-2.4.82C7.33 2.314 6.504 2 5.6 2c-2.206 0-4 1.794-4 4 0 1.112.458 2.118 1.194 2.844C1.146 9.604 0 11.266 0 13.2c0 .884.716 1.6 1.6 1.6h12.8c.884 0 1.6-.716 1.6-1.6 0-1.934-1.146-3.596-2.794-4.356z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g>
<g transform="translate(-1228 -1456) translate(1216 1416) translate(12 40)">
<mask id="6szqyv7o1b" fill="#fff">
<use xlink:href="#rt3mruthma"/>
</mask>
<use fill="#878190" xlink:href="#rt3mruthma"/>
<g fill="#878190" mask="url(#6szqyv7o1b)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 12.8"><path d="M11.2,12.8A4.79,4.79,0,0,0,10,9.6H11.2a3.21,3.21,0,0,1,3.2,3.2Zm-9.6,0A3.21,3.21,0,0,1,4.8,9.6H6.4a3.21,3.21,0,0,1,3.2,3.2Zm4-9.6A2.4,2.4,0,1,1,3.2,5.6,2.39,2.39,0,0,1,5.6,3.2Zm3.45.42A2.35,2.35,0,0,1,10.4,3.2a2.4,2.4,0,1,1,0,4.8,2.35,2.35,0,0,1-1.35-.42,3.84,3.84,0,0,0,0-4Zm4.16,4.82A4,4,0,0,0,8,2.42,4,4,0,0,0,5.6,1.6,4,4,0,0,0,2.79,8.44,4.82,4.82,0,0,0,0,12.8a1.6,1.6,0,0,0,1.6,1.6H14.4A1.6,1.6,0,0,0,16,12.8a4.82,4.82,0,0,0-2.79-4.36Z" transform="translate(0 -1.6)" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 569 B

@@ -22,11 +22,6 @@
Account created:
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
</div>
<div>
Most recent cron:
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
("auth.timestamps.loggedin")
</div>
<div v-if="cronError">
"lastCron" value:
<strong>{{ hero.lastCron | formatDate }}</strong>
@@ -36,6 +31,19 @@
("auth.timestamps.loggedin" and "lastCron" dates are different).
</span>
</div>
<div class="form-inline">
<div>
Most recent cron:
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
("auth.timestamps.loggedin")
</div>
<button
class="btn btn-primary ml-2"
@click="resetCron()"
>
Reset Cron to Yesterday
</button>
</div>
<div class="subsection-start">
Time zone:
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
@@ -218,6 +226,10 @@ export default {
await this.saveHero({ hero: this.hero, msg: 'API Token' });
this.tokenModified = true;
},
resetCron () {
this.hero.resetCron = true;
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
},
},
};
</script>
@@ -17,6 +17,10 @@
:reset-counter="resetCounter"
/>
<subscription-and-perks
:hero="hero"
/>
<cron-and-auth
:hero="hero"
:reset-counter="resetCounter"
@@ -97,6 +101,7 @@ import AvatarAndDrops from './avatarAndDrops';
import PrivilegesAndGems from './privilegesAndGems';
import ContributorDetails from './contributorDetails';
import Transactions from './transactions';
import SubscriptionAndPerks from './subscriptionAndPerks';
import { userStateMixin } from '../../../mixins/userState';
@@ -110,6 +115,7 @@ export default {
PrivilegesAndGems,
ContributorDetails,
Transactions,
SubscriptionAndPerks,
},
mixins: [userStateMixin],
data () {
@@ -0,0 +1,155 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Subscription, Monthly Perks
</h3>
<div v-if="expand">
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
<div v-if="hero.purchased.plan.paymentMethod">
Payment method:
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
</div>
<div v-if="hero.purchased.plan.planId">
Payment schedule ("basic-earned" is monthly):
<strong>{{ hero.purchased.plan.planId }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCreated">
Creation date:
<strong>{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
</div>
<div>
Termination date:
<strong
v-if="hero.purchased.plan.dateTerminated"
>
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
</strong>
<strong v-else> None </strong>
</div>
<div class="form-inline">
<label>
Consecutive months:
<input
v-model="hero.purchased.plan.consecutive.count"
class="form-control"
type="number"
min="0"
step="1"
>
</label>
</div>
<div>
Months until renewal:
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
</div>
<div>
Next Mystic Hourglass:
<strong>{{ nextHourglassDate }}</strong>
</div>
<div class="form-inline">
<label>
Mystic Hourglasses:
<input
v-model="hero.purchased.plan.consecutive.trinkets"
class="form-control"
type="number"
min="0"
step="1"
>
</label>
</div>
<div>
Gem cap:
<strong>{{ hero.purchased.plan.consecutive.gemCapExtra + 25 }}</strong>
</div>
<div class="form-inline">
<label>
Gems bought this month:
<input
v-model="hero.purchased.plan.gemsBought"
class="form-control"
type="number"
min="0"
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
step="1"
>
</label>
</div>
<div
v-if="hero.purchased.plan.extraMonths > 0"
>
Additional credit (applied upon cancellation):
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
</div>
<div>
Mystery Items:
<span
v-if="hero.purchased.plan.mysteryItems.length > 0"
>
<span
v-for="(item, index) in hero.purchased.plan.mysteryItems"
:key="index"
>
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
{{ item }},
</strong>
<strong v-else> {{ item }} </strong>
</span>
</span>
<span v-else>
<strong>None</strong>
</span>
</div>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
</form>
</div>
</div>
</template>
<script>
import moment from 'moment';
import saveHero from '../mixins/saveHero';
import { getPlanContext } from '@/../../common/script/cron';
export default {
mixins: [saveHero],
props: {
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
};
},
computed: {
nextHourglassDate () {
const currentPlanContext = getPlanContext(this.hero, new Date());
return currentPlanContext.nextHourglassDate.format('MMMM');
},
},
watch: {
'hero.purchased.plan.consecutive.count' () { // eslint-disable-line object-shorthand
this.hero.purchased.plan.consecutive.gemCapExtra = Math.min(
Math.floor(this.hero.purchased.plan.consecutive.count / 3) * 5, 25,
);
},
},
methods: {
dateFormat (date) {
return moment(date).format('YYYY/MM/DD');
},
},
};
</script>
File diff suppressed because it is too large Load Diff
@@ -1,21 +1,5 @@
<template>
<div class="form">
<div class="form-group row text-center">
<div class="col-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('facebook')"
>
<div
class="svg-icon social-icon"
v-html="icons.facebookIcon"
></div>
<span>{{ registering
? $t('signUpWithSocial', {social: 'Facebook'})
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
</div>
</div>
</div>
<div class="form-group row text-center">
<div class="col-12">
<div
@@ -243,7 +227,6 @@ import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
@@ -260,7 +243,6 @@ export default {
};
data.icons = Object.freeze({
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
@@ -308,8 +290,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
@@ -621,7 +621,6 @@ import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import exclamation from '@/assets/svg/exclamation.svg';
import gryphon from '@/assets/svg/gryphon.svg';
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
@@ -644,7 +643,6 @@ export default {
exclamation,
gryphon,
habiticaIcon,
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
@@ -734,8 +732,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
@@ -757,8 +753,7 @@ export default {
}, 500),
sanitizeRedirect (redirect) {
if (!redirect) return '/';
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
sanitizedString = `/${sanitizedString}`;
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
return sanitizedString;
},
async register () {
@@ -27,7 +27,7 @@
<customize-options
:items="set.options"
:current-value="user.preferences.hair.color"
:full-set="!hideSet(set) && !userOwnsSet('hair', set.keys, 'color')"
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
/>
</div>
@@ -23,7 +23,7 @@
<customize-options
:items="set.options"
:current-value="user.preferences.skin"
:full-set="!hideSet(set) && !userOwnsSet('skin', set.keys)"
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
/>
</div>
@@ -112,10 +112,8 @@ export default {
},
},
watch: {
'group._id': {
async groupId () {
this.loadChallenges();
},
'group._id': function groupId () {
this.loadChallenges();
},
},
mounted () {
@@ -1,3 +1,4 @@
<!-- THIS IS A VERY OLD FILE DO NOT USE -->
<template>
<div class="create-group-modal-pages">
<div
@@ -0,0 +1,356 @@
<template>
<b-modal
id="create-group"
:title="activePage === PAGES.CREATE_GROUP ? 'Create your Group' : 'Select Payment'"
:hide-footer="true"
:hide-header="true"
size="md"
@hide="onHide()"
>
<div
v-if="activePage === PAGES.CREATE_GROUP"
class="col-12"
>
<!-- HEADER -->
<div
class="modal-close"
>
<span
class="cancel-text"
@click="close()"
>
{{ $t('cancel') }}
</span>
<button
class="btn btn-primary next-button"
:value="$t('next')"
:disabled="!newGroupIsReady"
@click="createGroup()"
>
{{ $t('next') }}
</button>
</div>
<h2>{{ $t('createGroup') }}</h2>
<!-- FORM -->
<div class="form-group">
<lockable-label
:text="$t('nameStar')"
/>
<input
id="new-group-name"
v-model="newGroup.name"
class="form-control input-medium option-content name-input"
required="required"
type="text"
:placeholder="$t('nameStarText')"
>
</div>
<div class="form-group">
<lockable-label
for="new-group-description"
:text="$t('descriptionOptional')"
class="description-label"
/>
<div class="characters-remaining">
{{ $t('charactersRemaining', {characters: charactersRemaining}) }}
</div>
<textarea
id="new-group-description"
v-model="newGroup.description"
class="form-control option-content description-input"
cols="3"
:placeholder="$t('descriptionOptionalText')"
maxlength="250"
></textarea>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input
id="create-group-leaderOnlyChallenges-checkbox"
v-model="newGroup.leaderOnly.challenges"
class="custom-control-input"
type="checkbox"
>
<label
class="custom-control-label"
for="create-group-leaderOnlyChallenges-checkbox"
>{{ $t('leaderOnlyChallenges') }}</label>
</div>
</div>
<div class="form-group">
<lockable-label
:text="$t('groupUse')"
/>
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"
class="group-input"
:placeholder="'groupUseDefault'"
:value="newGroup.demographics"
@select="newGroup.demographics = $event"
/>
</div>
<div class="form-group">
<button
class="btn btn-primary btn-lg btn-block btn-payment"
:disabled="!newGroupIsReady"
@click="createGroup()"
>
{{ $t('nextPaymentMethod') }}
</button>
</div>
</div>
<!-- PAYMENT -->
<!-- @TODO: Separate payment into a separate modal -->
<div
v-if="activePage === PAGES.PAY"
class="col-12 payments"
>
<div class="text-center">
<payments-buttons
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
:amazon-data="pay(PAYMENTS.AMAZON)"
/>
</div>
</div>
</b-modal>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $purple-300;
font-size: 1.25rem;
height: 28px;
width: 120px;
margin-top: 24px;
align-self: center;
}
.cancel-text {
color: $blue-10;
font-size: 0.875rem;
margin-right: 16px;
text-align: center;
cursor: pointer;
}
.next-button {
align-self: center;
}
.form-control::placeholder {
color: $gray-50;
height: 32px;
}
.description-label {
margin-bottom: -24px;
}
.name-input, .description-input, .group-input {
margin-top: -4px;
}
.characters-remaining {
color: $gray-100;
font-size: 0.75rem;
line-height: 1.33;
text-align: right;
margin-bottom: 12px;
}
.description-input {
height: 56px;
}
.spacer {
margin-bottom: 16px !important;
}
.btn-payment {
margin: 24px 112px 24px 112px;
width: 177px;
}
.payments {
padding: 24px;
}
.payment-options {
margin-bottom: 4em;
.purple-box {
background-color: #4f2a93;
color: #fff;
padding: .5em;
border-radius: 8px;
width: 200px;
height: 215px;
.dollar {
}
.number {
font-size: 60px;
}
.name {
width: 100px;
margin-left: .3em;
}
div {
display: inline-block;
}
}
.box, .purple-box {
display: inline-block;
vertical-align: bottom;
}
}
</style>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
#create-group {
.modal-dialog {
max-width: 448px;
}
.modal-content {
width: 448px;
max-height: 436px;
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: 0px;
margin-left: 12px;
margin-right: 12px;
}
.modal-close {
position: absolute;
right: 16px;
cursor: pointer;
top: 0px;
}
}
</style>
<script>
import paymentsMixin from '../../mixins/payments';
import { mapState } from '@/libs/store';
import paymentsButtons from '@/components/payments/buttons/list';
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import * as Analytics from '@/libs/analytics';
export default {
components: {
paymentsButtons,
selectTranslatedArray,
lockableLabel,
},
mixins: [paymentsMixin],
data () {
return {
amazonPayments: {},
PAGES: {
CREATE_GROUP: 'create-group',
// UPGRADE_GROUP: 'upgrade-group',
PAY: 'pay',
},
PAYMENTS: {
AMAZON: 'amazon',
STRIPE: 'stripe',
},
paymentMethod: '',
newGroup: {
type: 'guild',
privacy: 'private',
name: '',
description: '',
leaderOnly: {
challenges: false,
},
demographics: null,
user: '',
},
activePage: 'create-group',
type: 'guild',
};
},
computed: {
...mapState({ user: 'user.data' }),
newGroupIsReady () {
return Boolean(this.newGroup.name) && Boolean(this.newGroup.demographics);
},
charactersRemaining () {
const currentLength = this.newGroup.description ? this.newGroup.description.length : 0;
return 250 - currentLength;
},
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'create-group');
},
changePage (page) {
this.activePage = page;
},
createGroup () {
this.changePage(this.PAGES.PAY);
},
pay (paymentMethod) {
const subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
const demographicsKey = this.newGroup.demographics;
const paymentData = {
subscription: subscriptionKey,
coupon: null,
demographics: demographicsKey,
};
Analytics.track({
hitType: 'event',
eventName: 'group plan create',
eventAction: 'group plan create',
eventCategory: 'behavior',
demographics: this.newGroup.demographics,
type: this.newGroup.type,
}, { trackOnClient: true });
if (this.upgradingGroup && this.upgradingGroup._id) {
paymentData.groupId = this.upgradingGroup._id;
paymentData.group = this.upgradingGroup;
} else {
paymentData.groupToCreate = this.newGroup;
}
this.paymentMethod = paymentMethod;
if (this.paymentMethod === this.PAYMENTS.AMAZON) {
paymentData.type = 'subscription';
return paymentData;
}
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
this.redirectToStripe(paymentData);
}
return null;
},
onHide () {
this.sendingInProgress = false;
},
},
};
</script>
@@ -0,0 +1,204 @@
<template>
<b-modal
id="group-plans-update"
title="New Shared Task Board"
size="lg"
hide-footer="hide-footer"
:no-close-on-backdrop="true"
:no-close-on-esc="true"
:centered="true"
>
<div
slot="modal-header"
class="w-100 d-flex pt-2 justify-content-center"
>
<h2
class="mx-auto mt-4"
v-once
>
{{ $t('newGroupsWelcome') }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
aria-hidden="true"
tabindex="0"
@click="close()"
@keypress.enter="close()"
v-html="icons.close"
></div>
<img
class="task-columns"
src="~@/assets/images/group-plans/task-columns.png"
srcset="~@/assets/images/group-plans/task-columns@2x.png 2x,
~@/assets/images/group-plans/task-columns@3x.png 3x"
>
</div>
<div
class="d-flex flex-column justify-content-center p-4"
>
<div class="d-flex align-items-center justify-content-center mb-3">
<div
class="svg-icon sparkles my-auto mr-3"
v-html="icons.sparkles"
>
</div>
<h3
class="my-auto"
v-once
>
{{ $t('newGroupsWhatsNew') }}
</h3>
<div
class="svg-icon sparkles my-auto ml-3 flip-x"
v-html="icons.sparkles"
>
</div>
</div>
<ul
class="mb-4 px-4"
>
<li>{{ $t('newGroupsBullet01') }}</li>
<li>{{ $t('newGroupsBullet02') }}</li>
<li>{{ $t('newGroupsBullet03') }}</li>
<li>{{ $t('newGroupsBullet04') }}</li>
<li>{{ $t('newGroupsBullet05') }}</li>
<li>{{ $t('newGroupsBullet06') }}</li>
<li>{{ $t('newGroupsBullet07') }}</li>
<li>{{ $t('newGroupsBullet08') }}</li>
<li>{{ $t('newGroupsBullet09') }}</li>
<li>{{ $t('newGroupsBullet10') }}
<ul class="p-0">
<li v-html="$t('newGroupsBullet10a')"></li>
<li v-html="$t('newGroupsBullet10b')"></li>
<li v-html="$t('newGroupsBullet10c')"></li>
</ul>
</li>
</ul>
<div
class="mx-auto"
v-html="$t('newGroupsVisitFAQ')"
></div>
<div
class="mx-auto"
>
{{ $t('newGroupsEnjoy') }}
</div>
<button
class="btn btn-primary mt-4 mb-1 mx-auto"
@click="close()"
@keypress.enter="close()"
>
{{ $t('getStarted') }}
</button>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#group-plans-update {
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-dialog {
max-width: 566px;
margin-top: 0px;
}
.modal-header {
background-image: linear-gradient(64deg, $purple-200, $purple-300 55%);
height: 136px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $white;
line-height: 28px;
}
li {
padding-left: 0.75rem;
}
ul {
line-height: 24px;
list-style-type: square;
li ul {
list-style-type: none;
li::before {
margin-right: 1rem;
font-size: 16px;
content: '▫';
}
}
}
.btn-primary {
width: max-content;
}
.close-x {
color: $purple-400;
height: 16px;
width: 16px;
position: absolute;
opacity: 0.75;
cursor: pointer;
right: 18px;
top: 18px;
&:hover, &:focus {
opacity: 1;
}
}
.sparkles {
width: 32px;
height: 17px;
&.flip-x {
transform: scaleX(-1);
}
}
.task-columns {
position: absolute;
top: 84px;
}
</style>
<script>
import closeIcon from '@/assets/svg/close.svg';
import sparkles from '@/assets/svg/sparkles-left.svg';
export default {
data () {
return {
icons: Object.freeze({
close: closeIcon,
sparkles,
}),
};
},
methods: {
close () {
this.$store.dispatch('user:set', { 'flags.tour.groupPlans': -2 });
this.$root.$emit('bv::hide::modal', 'group-plans-update');
},
},
};
</script>
@@ -28,7 +28,7 @@
{{ $t('groupBilling') }}
</router-link>
</secondary-menu>
<div class="col-12">
<div class="col-12 px-0">
<router-view />
</div>
</div>
@@ -1,5 +1,8 @@
<template>
<div class="standard-page">
<div
class="standard-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<group-plan-overview-modal />
<task-modal
ref="taskModal"
@@ -7,65 +10,80 @@
:purpose="taskFormPurpose"
:group-id="groupId"
@cancel="cancelTaskModal()"
@taskCreated="taskCreated"
@taskEdited="taskEdited"
@taskCreated="loadTasks"
@taskEdited="loadTasks"
@taskDestroyed="taskDestroyed"
/>
<div class="row tasks-navigation">
<div class="col-12 col-md-4">
<h1>{{ $t('groupTasksTitle') }}</h1>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="d-flex flex-wrap align-items-center mb-4">
<div class="mr-auto">
<h1>{{ group.name }}</h1>
</div>
<!-- @TODO: Abstract to component!-->
<div class="col-12 col-md-4">
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
</div>
<div
v-if="canCreateTasks"
class="create-task-area d-flex"
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
<transition name="slide-tasks-btns">
<div
class="d-flex flex-wrap align-items-center justify-content-end ml-auto"
>
<toggle-switch
id="taskMirrorToggle"
class="mr-3 mb-1 ml-auto"
:label="'Copy tasks'"
:checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1"
:hover-text="'Show assigned and open tasks on your personal task board'"
@change="changeMirrorPreference"
/>
<div
class="day-start d-flex align-items-center"
v-html="$t('dayStart', { startTime: groupStartTime } )"
>
</div>
<div class="ml-2">
<button
id="create-task-btn"
v-if="canCreateTasks"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</button>
<div
v-if="openCreateBtn"
class="d-flex"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
class="svg-icon"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('create') }}
</b-tooltip>
</div>
</div>
<div class="row">
@@ -79,6 +97,7 @@
:search-text="searchText"
:draggable-override="canCreateTasks"
@editTask="editTask"
@taskSummary="taskSummary"
@loadGroupCompletedTodos="loadGroupCompletedTodos"
@taskDestroyed="taskDestroyed"
/>
@@ -86,12 +105,58 @@
</div>
</template>
<style lang="scss">
#taskMirrorToggle {
font-weight: bold;
.svg-icon {
margin: 3px 6px 0px 4px;
}
.toggle-switch {
margin-left: 0px;
}
.toggle-switch-description {
margin-top: 3px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/create-task.scss';
.tasks-navigation {
margin-bottom: 40px;
h1 {
color: $purple-300;
margin-bottom: 0px;
}
.day-start {
height: 2rem;
padding: 0.25rem 0.75rem;
border-radius: 2px;
color: $gray-100;
background-color: $gray-600;
}
@media screen and (min-width: 1200px) {
.input-search {
margin-left: 12.5rem;
width: 25%;
}
}
@media screen and (max-width: 1200px) {
.input-search {
width: 50%;
}
}
@media screen and (max-width: 500px) {
#create-task-btn {
margin-top: 4px;
}
}
.positive {
@@ -107,11 +172,15 @@
import Vue from 'vue';
import cloneDeep from 'lodash/cloneDeep';
import findIndex from 'lodash/findIndex';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import TaskColumn from '../tasks/column';
import TaskModal from '../tasks/taskModal';
import TaskSummary from '../tasks/taskSummary';
import GroupPlanOverviewModal from './groupPlanOverviewModal';
import toggleSwitch from '@/components/ui/toggleSwitch';
import sync from '../../mixins/sync';
import positiveIcon from '@/assets/svg/positive.svg';
import filterIcon from '@/assets/svg/filter.svg';
@@ -121,14 +190,18 @@ import dailyIcon from '@/assets/svg/daily.svg';
import todoIcon from '@/assets/svg/todo.svg';
import rewardIcon from '@/assets/svg/reward.svg';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
GroupPlanOverviewModal,
toggleSwitch,
},
mixins: [sync],
props: ['groupId'],
data () {
return {
@@ -199,6 +272,16 @@ export default {
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
groupStartTime () {
if (!this.group || !this.group.cron) return null;
const { dayStart, timezoneOffset } = this.group.cron;
const timezoneDiff = this.user.preferences.timezoneOffset - timezoneOffset;
return moment()
.hour(dayStart)
.minute(0)
.subtract(timezoneDiff, 'minutes')
.format('h:mm A');
},
},
watch: {
// call again the method if the route changes (when this route is already active)
@@ -208,6 +291,10 @@ export default {
this.$set(this, 'searchId', to.params.groupId);
next();
},
async beforeRouteLeave (to, from, next) {
await this.sync();
next();
},
mounted () {
if (!this.searchId) this.searchId = this.groupId;
this.load();
@@ -218,6 +305,7 @@ export default {
this.$root.$on('habitica:team-sync', () => {
this.loadTasks();
this.loadGroupCompletedTodos();
});
},
methods: {
@@ -238,6 +326,9 @@ export default {
this.group.members = members;
this.loadTasks();
if (this.user.flags.tour.groupPlans !== -2) {
this.$root.$emit('bv::show::modal', 'group-plans-update');
}
},
async loadTasks () {
this.tasksByType = {
@@ -251,22 +342,13 @@ export default {
groupId: this.searchId,
});
const groupedApprovals = await this.loadApprovals();
tasks.forEach(task => {
if (
groupedApprovals[task._id]
&& groupedApprovals[task._id].length > 0
) task.approvals = groupedApprovals[task._id];
this.tasksByType[task.type].push(task);
});
},
async loadApprovals () {
const approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
groupId: this.searchId,
});
return groupBy(approvalRequests, 'group.taskId');
if (this.editingTask && this.editingTask.completed) {
this.loadGroupCompletedTodos();
}
},
editTask (task) {
this.taskFormPurpose = 'edit';
@@ -277,6 +359,12 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
async loadGroupCompletedTodos () {
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
groupId: this.searchId,
@@ -299,14 +387,6 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskCreated (task) {
task.group.id = this.group._id;
this.tasksByType[task.type].unshift(task);
},
taskEdited (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1, task);
},
taskDestroyed (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1);
@@ -354,6 +434,25 @@ export default {
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
} else { // we're turning copy OFF for this group
groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1);
}
this.$store.dispatch('user:set', {
'preferences.tasks.mirrorGroupTasks': groupsToMirror,
});
},
},
};
</script>
@@ -1,6 +1,7 @@
<template>
<!-- @TODO: Move to group plans folder-->
<div>
<group-plan-creation-modal />
<div>
<div class="header">
<h1 class="text-center">
@@ -51,6 +52,7 @@
</div>
</div>
</div>
<!-- Upgrading an existing group -->
<div
v-if="upgradingGroup._id"
id="upgrading-group"
@@ -92,7 +94,6 @@
</div>
</div>
<div class="box payment-providers">
<h3>Choose your payment method</h3>
<payments-buttons
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
:amazon-data="pay(PAYMENTS.AMAZON)"
@@ -101,154 +102,53 @@
</div>
</div>
</div>
<!-- Create a new group -->
<div
v-if="!upgradingGroup._id"
class="container col-6 offset-3 create-option"
>
<div class="row">
<h1 class="col-12 text-center purple-header">
Create your Group today!
Create Your Group Today!
</h1>
</div>
<div class="row">
<div class="col-12 text-center">
<button
class="btn btn-primary create-group"
@click="launchModal('create')"
@click="launchModal('create-page')"
>
Create Your New Group
Create Your New Group!
</button>
</div>
</div>
<div class="row pricing">
<div class="col-5">
<div class="dollar">
$
</div>
<div class="number">
9
</div>
<div class="name">
<div>Group Owner</div>
<div>Subscription</div>
</div>
<div class="row pricing justify-content-center align-items-center">
<div class="dollar">
$
</div>
<div class="col-1">
<div class="plus">
+
</div>
<div class="number">
9
</div>
<div class="col-6">
<div class="dollar">
$
</div>
<div class="number">
3
</div>
<div class="name">
<div>Each Additional</div>
<div>Member</div>
</div>
<div class="name">
<div>Group Owner</div>
<div>Subscription</div>
</div>
<div class="plus">
+
</div>
<div class="dollar">
$
</div>
<div class="number">
3
</div>
<div class="name">
<div>Each Additional</div>
<div>Member</div>
</div>
</div>
</div>
</div>
<b-modal
id="group-plan-modal"
:title="activePage === PAGES.CREATE_GROUP ? 'Create your Group' : 'Select Payment'"
size="md"
hide-footer="hide-footer"
>
<div
v-if="activePage === PAGES.CREATE_GROUP"
class="col-12"
>
<div class="form-group">
<label
class="control-label"
for="new-group-name"
>Name</label>
<input
id="new-group-name"
v-model="newGroup.name"
class="form-control input-medium option-content"
required="required"
type="text"
placeholder="Name"
>
</div>
<div class="form-group">
<label for="new-group-description">{{ $t('description') }}</label>
<textarea
id="new-group-description"
v-model="newGroup.description"
class="form-control option-content"
cols="3"
:placeholder="$t('description')"
></textarea>
</div>
<div
v-if="type === 'guild'"
class="form-group"
>
<div class="custom-control custom-radio">
<input
v-model="newGroup.privacy"
class="custom-control-input"
type="radio"
name="new-group-privacy"
value="private"
>
<label class="custom-control-label">{{ $t('inviteOnly') }}</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input
id="create-group-leaderOnlyChallenges-checkbox"
v-model="newGroup.leaderOnly.challenges"
class="custom-control-input"
type="checkbox"
>
<label
class="custom-control-label"
for="create-group-leaderOnlyChallenges-checkbox"
>{{ $t('leaderOnlyChallenges') }}</label>
</div>
</div>
<div
v-if="type === 'party'"
class="form-group"
>
<button
class="btn btn-secondary form-control"
:value="$t('create')"
@click="createGroup()"
></button>
</div>
<div class="form-group">
<button
class="btn btn-primary btn-lg btn-block"
:disabled="!newGroupIsReady"
@click="createGroup()"
>
{{ $t('create') }}
</button>
</div>
</div>
<div
v-if="activePage === PAGES.PAY"
class="col-12"
>
<div class="text-center">
<h3>Choose your payment method</h3>
<payments-buttons
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
:amazon-data="pay(PAYMENTS.AMAZON)"
/>
</div>
</div>
</b-modal>
</div>
</template>
@@ -260,12 +160,17 @@
.dollar {
position: absolute;
left: -1em;
top: 1em;
left: -16px;
top: 16px;
}
.purple-box {
color: #bda8ff;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
}
.number {
@@ -279,6 +184,8 @@
.payment-providers {
width: 350px;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
}
@@ -286,9 +193,9 @@
background: #432874;
background: linear-gradient(180deg, #4F2A93 0%, #432874 100%);
color: #fff;
padding: 2em;
padding: 32px;
height: 340px;
margin-bottom: 2em;
margin-bottom: 32px;
margin-left: -12px;
margin-right: -12px;
@@ -312,6 +219,7 @@
.box {
height: 416px;
border-radius: 8px;
}
h2 {
@@ -354,17 +262,19 @@
button.create-group {
width: 330px;
height: 96px;
border-radius: 8px;
font-size: 1.5rem;
}
.purple-header {
color: #6133b4;
font-size: 48px;
margin-top: 1em;
margin-top: 16px;
}
.pricing {
margin-top: 2em;
margin-bottom: 4em;
margin-top: 32px;
margin-bottom: 64px;
.dollar, .number, .name {
display: inline-block;
@@ -373,35 +283,41 @@
}
.plus {
font-size: 34px;
font-size: 2.125rem;
color: #a5a1ac;
margin-left: 16px;
margin-right: 16px;
}
.dollar {
margin-bottom: 1.5em;
font-size: 32px;
margin-bottom: 24px;
font-size: 2rem;
font-weight: bold;
}
.name {
font-size: 24px;
margin-bottom: .8em;
margin-left: .5em;
font-size: 1.5rem;
margin-left: 8px;
margin-right: 8px;
}
.number {
font-size: 72px;
font-size: 4.5rem;
font-weight: bolder;
}
}
.payment-options {
margin-bottom: 4em;
margin-bottom: 64px;
h4 {
color: #34313a;
}
.purple-box {
background-color: #4f2a93;
color: #fff;
padding: .5em;
padding: 8px;
border-radius: 8px;
width: 200px;
height: 215px;
@@ -415,7 +331,7 @@
.name {
width: 100px;
margin-left: .3em;
margin-left: 4.8px;
}
.plus {
@@ -440,10 +356,12 @@ import paymentsMixin from '../../mixins/payments';
import { mapState } from '@/libs/store';
import positiveIcon from '@/assets/svg/positive.svg';
import paymentsButtons from '@/components/payments/buttons/list';
import groupPlanCreationModal from '../group-plans/groupPlanCreationModal';
export default {
components: {
paymentsButtons,
groupPlanCreationModal,
},
mixins: [paymentsMixin],
data () {
@@ -492,12 +410,7 @@ export default {
},
methods: {
launchModal () {
this.changePage(this.PAGES.CREATE_GROUP);
this.$root.$emit('bv::show::modal', 'group-plan-modal');
},
changePage (page) {
this.activePage = page;
window.scrollTo(0, 0);
this.$root.$emit('bv::show::modal', 'create-group');
},
createGroup () {
this.changePage(this.PAGES.PAY);
@@ -247,9 +247,6 @@
<li v-once>
{{ $t('sleepBullet3') }}
</li>
<li v-once>
{{ $t('sleepBullet4') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
@@ -342,6 +339,7 @@
<li>
<a
href
:style="glossary-link"
v-html="$t('glossary')"
></a>
</li>
@@ -536,6 +534,21 @@
margin-left: .5em;
}
// formats the report a bug link to match the others
a:not([href]) {
&:not([role=button]) {
color: #007bff;
text-decoration: none;
}
}
a:not([href]):hover {
&:not([role=button]) {
color: #0056b3;
text-decoration: underline;
}
}
.tier1-icon, .tier2-icon {
width: 11px;
}
@@ -23,7 +23,14 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push({ name: 'tasks' });
if (this.notification.data.groupId) {
this.$router.push({
name: 'groupPlanDetailTaskInformation',
params: { groupId: this.notification.data.groupId },
});
} else {
this.$router.push({ name: 'tasks' });
}
},
},
};
@@ -129,8 +129,6 @@ import GUILD_INVITATION from './notifications/guildInvitation';
import PARTY_INVITATION from './notifications/partyInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
@@ -155,8 +153,6 @@ export default {
PARTY_INVITATION,
CHALLENGE_INVITATION,
QUEST_INVITATION,
GROUP_TASK_APPROVAL,
GROUP_TASK_APPROVED,
GROUP_TASK_ASSIGNED,
GROUP_TASK_CLAIMED,
UNALLOCATED_STATS_POINTS,
@@ -182,7 +178,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVED',
'QUEST_INVITATION',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
@@ -196,8 +192,6 @@ export default {
'CHALLENGE_INVITATION',
'QUEST_INVITATION',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED',
'NEW_MYSTERY_ITEMS',
'CARD_RECEIVED',
@@ -32,11 +32,12 @@
/>
<onboarding-complete />
<first-drops />
<group-plans-update />
</div>
</template>
<style lang='scss'>
.introjs-helperNumberLayer, .introjs-bullets {
.introjs-helperNumberLayer, .introjs-bullets, .introjs-skipbutton {
display: none;
}
@@ -62,9 +63,9 @@
.npc_justin_textbox {
position: absolute;
right: 1em;
top: -3.6em;
top: -55px;
width: 48px;
height: 52px;
height: 51px;
background-image: url('~@/assets/images/justin_textbox.png');
}
}
@@ -95,7 +96,7 @@
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12) !important;
}
.introjs-skipbutton.btn-primary, .introjs-donebutton.btn-primary {
.introjs-donebutton.btn-primary {
color: #fff;
}
</style>
@@ -142,6 +143,7 @@ import loginIncentives from './achievements/login-incentives';
import onboardingComplete from './achievements/onboardingComplete';
import verifyUsername from './settings/verifyUsername';
import firstDrops from './achievements/firstDrops';
import groupPlansUpdate from './group-plans/groupPlansUpdateModal';
const NOTIFICATIONS = {
// general notifications
@@ -268,6 +270,7 @@ export default {
genericAchievement,
onboardingComplete,
firstDrops,
groupPlansUpdate,
},
mixins: [notifications, guide],
data () {
@@ -612,7 +615,7 @@ export default {
const yesterUtcOffset = yesterDay.utcOffset();
dailys.forEach(task => {
if (task && task.group.approval && task.group.approval.requested) return;
if (task.group && task.group.id) return;
if (task.completed) return;
const due = shouldDo(yesterDay, task, { timezoneUtcOffset: yesterUtcOffset });
if (task.yesterDaily && due) this.yesterDailies.push(task);
@@ -196,7 +196,7 @@ export default {
appState.group = pick(this.amazonPayments.groupToCreate, ['_id', 'memberCount', 'name']);
} else {
appState.newGroup = false;
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name']);
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name', 'type']);
}
} else if (paymentType && paymentType.indexOf('gift-') === 0) {
appState.gift = this.amazonPayments.gift;
@@ -153,14 +153,6 @@
</span>
</div>
</div>
<div class="row text-center">
<h4
v-once
class="col-12 text-payment mb-3"
>
{{ $t('choosePaymentMethod') }}
</h4>
</div>
<payments-buttons
:disabled="!selectedGemsBlock"
:stripe-fn="() => redirectToStripe({ gemsBlock: selectedGemsBlock })"
@@ -169,6 +161,19 @@
})"
:amazon-data="{type: 'single', gemsBlock: selectedGemsBlock}"
/>
<div
v-if="eventName === 'fall_extra_gems' || eventName === 'spooky_extra_gems'"
class="d-flex flex-column justify-content-center"
>
<h4 class="mt-3 mx-auto"> {{ $t('howItWorks') }}</h4>
<small class="text-center">
{{ $t('gemSaleHow', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
</small>
<h4 class="mt-3 mx-auto"> {{ $t('limitations') }}</h4>
<small class="text-center">
{{ $t('gemSaleLimitations', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
</small>
</div>
</div>
</b-modal>
</div>
@@ -178,6 +183,13 @@
@import '~@/assets/scss/colors.scss';
#buy-gems {
small {
color: $gray-100;
font-size: 12px;
margin-left: 20px;
margin-right: 20px;
}
.close-icon svg path {
stroke: $purple-400;
}
@@ -423,6 +435,15 @@ export default {
}
return '';
},
eventStartMonth () {
return moment(this.currentEvent.start).format('MMMM');
},
eventStartOrdinal () {
return moment(this.currentEvent.start).format('Do');
},
eventEndOrdinal () {
return moment(this.currentEvent.end).format('Do');
},
isGemsPromoActive () {
const currEvt = this.currentEvent;
if (currEvt && currEvt.gemsPromo && moment().isBefore(currEvt.end)) {
@@ -2,8 +2,8 @@
<b-modal
id="payments-success-modal"
:title="$t('accountSuspendedTitle')"
:hide-footer="isFromBalance"
:modal-class="isFromBalance ? ['modal-hidden-footer'] : []"
:hide-footer="isFromBalance || paymentData.newGroup"
:modal-class="isFromBalance || paymentData.newGroup ? ['modal-hidden-footer'] : []"
>
<div slot="modal-header">
<div class="check-container d-flex align-items-center justify-content-center">
@@ -16,15 +16,49 @@
<h2>{{ $t(isFromBalance ? 'success' : 'paymentSuccessful') }}</h2>
</div>
<div slot="modal-footer">
<!-- everyone else -->
<div
v-once
v-if="paymentData.paymentType !== 'groupPlan' || paymentData.newGroup"
class="small-text"
>
{{ $t('giftSubscriptionText4') }}
</div>
<!-- upgradedGroup -->
<div
v-else
class="demographics d-flex flex-column justify-content-center"
>
<lockable-label
:text="$t('groupUse')"
class="mx-auto label-text"
/>
<select-translated-array
:items="[
'groupParentChildren',
'groupCouple',
'groupFriends',
'groupCoworkers',
'groupManager',
'groupTeacher'
]"
class="group-input"
:placeholder="'groupUseDefault'"
:value="upgradedGroup.demographics"
@select="upgradedGroup.demographics = $event"
/>
<button
v-if="!paymentData.newGroup"
class="btn btn-primary mx-auto"
:disabled="!upgradedGroup.demographics"
@click="submit()"
>
{{ $t('submit') }}
</button>
</div>
</div>
<div class="row">
<div class="col-12 modal-body-col">
<!-- buy gems for self -->
<template v-if="paymentData.paymentType === 'gems'">
<strong v-once>{{ $t('paymentYouReceived') }}</strong>
<div class="details-block gems">
@@ -36,6 +70,7 @@
<span>{{ paymentData.gemsBlock.gems }}</span>
</div>
</template>
<!-- buy or gift gems to someone else -->
<template
v-if="paymentData.paymentType === 'gift-gems'
|| paymentData.paymentType === 'gift-gems-balance'"
@@ -50,12 +85,14 @@
<span>{{ paymentData.gift.gems.amount }}</span>
</div>
</template>
<!-- give gift subscription (non-recurring)-->
<template v-if="paymentData.paymentType === 'gift-subscription'">
<span
v-html="$t('paymentYouSentSubscription', {
name: paymentData.giftReceiver, months: paymentData.subscription.months})"
></span>
</template>
<!-- buy self subscription (recurring) -->
<template v-if="paymentData.paymentType === 'subscription'">
<strong v-once>{{ $t('nowSubscribed') }}</strong>
<div class="details-block">
@@ -65,31 +102,45 @@
></span>
</div>
</template>
<!-- group plan new or upgraded -->
<template v-if="paymentData.paymentType === 'groupPlan'">
<span
v-html="$t(paymentData.newGroup
? 'groupPlanCreated' : 'groupPlanUpgraded', {groupName: paymentData.group.name})"
></span>
<div class="details-block">
<span
v-html="$t('paymentSubBilling', {
amount: groupPlanCost, months: paymentData.subscription.months})"
></span>
<div
v-if="!paymentData.newGroup || paymentData.newGroup"
class=""
>
<div class="details-block group-billing-date">
<span
v-html="$t('groupsPaymentSubBilling', { renewalDate })"
>
</span>
</div>
<div class="small-text group-auto-renew">
<span
v-once
>{{ $t('groupsPaymentAutoRenew') }}
</span>
</div>
</div>
</template>
<!-- buy self subscription auto renew -->
<template
v-if="paymentData.paymentType === 'groupPlan'
|| paymentData.paymentType === 'subscription'"
v-if="paymentData.paymentType === 'subscription'"
>
<span
v-once
class="small-text auto-renew"
>{{ $t('paymentAutoRenew') }}</span>
</template>
<!-- buttons for subscriptions -->
<button
v-if="paymentData.paymentType !== 'groupPlan'"
v-once
class="btn btn-primary"
@click="close()"
@click="submit()"
>
{{ $t('onwards') }}
</button>
@@ -102,7 +153,7 @@
@import '~@/assets/scss/colors.scss';
#payments-success-modal .modal-md {
max-width: 20.5rem;
max-width: 448px;
}
#payments-success-modal .modal-content {
@@ -114,6 +165,7 @@
border-bottom-left-radius: 8px;
}
#payments-success-modal .modal-header {
justify-content: center;
padding-top: 24px;
@@ -124,14 +176,14 @@
border-bottom: none;
h2 {
color: white;
color: $green-1;
}
.check-container {
width: 64px;
height: 64px;
border-radius: 50%;
background: #1CA372;
background: $green-1;
margin: 0 auto;
margin-bottom: 16px;
}
@@ -139,14 +191,13 @@
.check {
width: 35.1px;
height: 28px;
color: white;
color: $white;
}
}
#payments-success-modal .modal-body {
padding-top: 16px;
padding-bottom: 24px;
background: white;
padding: 16px 32px 24px 32px;
background: $white;
.modal-body-col {
display: flex;
@@ -162,9 +213,9 @@
.details-block {
background: $gray-700;
border-radius: 4px;
padding: 8px 24px;
padding: 8px 16px;
margin-top: 16px;
display: flex;
display: inline-flex;
flex-direction: row;
text-align: center;
@@ -188,6 +239,14 @@
color: $orange-10;
font-style: normal;
}
.group-auto-renew {
margin: 12px 20px -8px 20px;
color: $yellow-5;
font-style: normal;
}
.group-billing-date {
width: 269px;
}
}
#payments-success-modal .modal-footer {
@@ -201,14 +260,49 @@
font-style: normal;
}
}
.demographics {
background-color: $gray-700;
.label-text {
margin-bottom: 20px;
}
.group-input {
width: 400px !important;
margin-top: -24px !important;
}
.btn {
margin-top: 0px;
width: 77px;
margin-bottom: 20px;
}
}
</style>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
</style>
<script>
import checkIcon from '@/assets/svg/check.svg';
import gemIcon from '@/assets/svg/gem.svg';
import { mapState } from '@/libs/store';
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import paymentsMixin from '@/mixins/payments';
import * as Analytics from '@/libs/analytics';
export default {
components: {
selectTranslatedArray,
lockableLabel,
},
mixins: [paymentsMixin],
data () {
return {
icons: Object.freeze({
@@ -216,9 +310,14 @@ export default {
gem: gemIcon,
}),
paymentData: {},
upgradedGroup: {
name: '',
demographics: null,
},
};
},
computed: {
...mapState({ user: 'user.data', group: 'group.data' }),
groupPlanCost () {
const sub = this.paymentData.subscription;
const memberCount = this.paymentData.group.memberCount || 1;
@@ -242,7 +341,17 @@ export default {
this.$root.$off('habitica:payments-success');
},
methods: {
close () {
submit () {
if (this.paymentData.group && !this.paymentData.newGroup) {
Analytics.track({
hitType: 'event',
eventName: 'group plan upgrade',
eventAction: 'group plan upgrade',
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
},
@@ -387,9 +387,7 @@
{{ $t('saveAndConfirm') }}
</button>
</div>
<h5
v-if="user.auth.local.email"
>
<h5 v-if="user.auth.local.has_password">
{{ $t('changeEmail') }}
</h5>
<div
+1 -5
View File
@@ -94,6 +94,7 @@ export default {
'gems',
'bugs-features',
'world-boss',
'group-plans',
];
const hash = window.location.hash.replace('#', '');
@@ -105,11 +106,6 @@ export default {
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
},
visible: hash && headings.includes(hash) ? hash : null,
// @TODO webFaqStillNeedHelp: {
// linkStart: '[',
// linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)',
// },
// "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the <%= linkStart %>Habitica Help guild<%= linkEnd %>! We're happy to help."
};
},
mounted () {
@@ -3,7 +3,7 @@
<div class="container-fluid">
<h1>Privacy Notice</h1>
<p class="strong pagemeta">
Last Updated: December 10, 2021
Last Updated September 19, 2022: Removed reference to Facebook login, which is no longer supported.
</p>
<p>
HabitRPG, Inc. (HabitRPG, we, us, or our) welcomes you. This privacy notice (the Privacy
@@ -39,8 +39,8 @@
In connection with the creation of an account on our Platforms, we collect account credentials such as
your email, username, and password. We use this account information to create your account, including to
verify your identity. We also use this information to manage your account, including your transactions. If
you choose to log into your account through Google, Apple or Facebook, we capture and store the User
ID and email address connected to the respective account, so we can verify your identity when you log in.
you choose to log into your account through Google or Apple, we capture and store the User ID and email
address connected to the respective account, so we can verify your identity when you log in.
</p>
<h3>User Content</h3>
<p>
@@ -33,7 +33,7 @@
@import '~@/assets/scss/colors.scss';
.home-header {
background: #6133b4 !important;
background: $purple-300 !important;
position: static;
box-shadow: none !important;
height: 100px !important;
@@ -46,13 +46,13 @@
.nav-item a {
font-size: 14px !important;
color: #d5c8ff !important;
color: $purple-600 !important;
padding-top: 2.8em !important;
}
.nav-item a:hover {
background: transparent !important;
color: #fff !important;
color: $white !important;
}
.nav-item .nav-link {
@@ -73,20 +73,20 @@
}
.white-header {
background: #fff !important;
background-color: #fff !important;
background: $white !important;
background-color: $white !important;
a {
color: #271b3d !important;
color: $header-dark-background !important;
}
a:hover {
color: #fff !important;
color: $white !important;
}
}
#purple-footer {
background-color: #271b3d;
background-color: $header-dark-background;
.row {
margin: 0;
@@ -94,19 +94,91 @@
footer, footer a {
background: transparent;
color: #d5c8ff;
color: $purple-500;
&:hover {
color: $white;
}
}
h3 {
color: $purple-400;
}
hr {
border-top-color: $purple-100;
}
.donate-text {
color: $purple-500;
}
.logo {
color: #bda8ff;
color: $purple-300;
}
.social-circle, .btn-contribute {
background: #36205d;
color: #bda8ff;
.colophon {
color: $purple-500;
}
.svg-icon {
color: #bda8ff;
.social-circle {
background: $purple-50;
color: $purple-500;
.instagram svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.twitter svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.facebook svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.tumblr svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
}
.btn-contribute {
background: $white;
box-shadow: none;
border-radius: 2px;
width: 175px;
height: 32px;
color: $gray-50;
text-align: center;
line-height: 1.71;
font-weight: bold;
font-size: 0.875rem;
vertical-align: middle;
padding: 0;
margin: 32px 0 32px 24px;
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
a {
display: flex;
}
.text{
display: inline-block;
vertical-align: bottom;
}
}
}
@@ -142,7 +214,7 @@
<style lang="scss" scoped>
#bottom-wrap.purple-4 {
background-color: #271b3d;
background-color: #271B3D;
}
#bottom-wrap {
@@ -1,59 +1,117 @@
<template>
<div>
<approval-modal :task="task" />
<div
class="gray-100"
>
<div
v-if="showStatus"
>
<div
v-for="completion in completionsList"
:key="completion.userId"
class="d-flex completion-row"
>
<div class="control">
<div
class="inner d-flex justify-content-center align-items-center p-auto"
:class="{interactive: iconClass(completion) !== 'lock'}"
@click="iconClass(completion) !== 'lock' ? needsWork(completion) : null"
>
<div
class="icon"
:class="iconClass(completion)"
v-html="icons[iconClass(completion)]"
>
</div>
</div>
</div>
<div
class="px-75 py-2 info"
>
<div>
<strong> @{{ completion.userName }} </strong>
</div>
<div
v-if='completion.completedDate'
:class="{'green-10': completion.completed}"
>
{{ completion.completedDateString }}
</div>
</div>
</div>
</div>
<div
v-if="!(userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit')"
class="claim-bottom-message d-flex align-items-center"
>
<div
class="mr-auto ml-2"
v-if="assignedUsersCount > 0"
class="svg-icon ml-2 users-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.users"
></div>
<div
class="mr-auto ml-1 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="message"
></div>
<div
v-if="!userIsAssigned && !task.completed"
class="ml-auto mr-2"
class="d-flex ml-auto mr-1 my-auto"
v-if="task.group.assignedUsers && ['daily','todo'].indexOf(task.type) !== -1"
>
<a
class="claim-color"
@click="claim()"
>{{ $t('claim') }}</a>
<span
v-if="assignedUsersCount > 1"
class="d-flex mr-1 my-auto"
>
<span
class="small-check"
v-if="!showStatus && completionsCount"
>
<div
class="svg-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.check"
>
</div>
</span>
<span
class="ml-1 mr-2 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-if="!showStatus && completionsCount"
>
{{ completionsCount }}/{{ assignedUsersCount }}
</span>
<a
v-if="assignedUsersCount > 1 && !showStatus"
class="blue-10"
@click="showStatus = !showStatus"
>
{{ $t('viewStatus') }}
</a>
<a
v-if="showStatus"
@click="showStatus = !showStatus"
>
{{ $t('close') }}
</a>
</span>
<span
v-if="assignedUsersCount === 1 && task.type === 'daily'
&& !task.completed && singleAssignLastDone"
class="mr-1 d-inline-flex"
>
<span
v-html="icons.lastComplete"
v-b-tooltip.hover.bottom="$t('lastCompleted')"
class="svg-icon color last-completed mr-1 my-auto"
:class="{'gray-200': completionsCount !== assignedUsersCount}"
>
</span>
<span
:class="{'green-10': completionsCount === assignedUsersCount}"
>
{{ formattedCompletionTime }}
</span>
</span>
</div>
<div
v-if="userIsAssigned && !approvalRequested && !task.completed"
class="ml-auto mr-2"
>
<a
class="unclaim-color"
@click="unassign()"
>{{ $t('removeClaim') }}</a>
</div>
</div>
<div
v-if="approvalRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="approve()"
>{{ $t('approveTask') }}</a>
<a @click="needsWork()">{{ $t('needsWork') }}</a>
</div>
<div
v-if="multipleApprovalsRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center"
>
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
</div>
<div
v-if="userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit'"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="$emit('claimRewards')"
>{{ $t('claimRewards') }}</a>
</div>
</div>
</template>
@@ -61,148 +119,220 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.claim-bottom-message {
background-color: $gray-700;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
color: $gray-200;
background-color: $gray-600;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
font-size: 12px;
line-height: 1.334;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
z-index: 9;
height: 24px;
.blue-10 {
color: $blue-10;
}
}
.approve-color {
color: $green-10 !important;
.completion-row {
height: 3rem;
background-color: $gray-700;
font-size: 12px;
.control {
background-color: $gray-200;
border-top: 1px solid $gray-100;
width: 40px;
height: 48px;
padding: 9px 6px;
.inner {
width: 28px;
height: 28px;
padding: 6px;
border-radius: 2px;
background-color: rgba($white, 0.5);
cursor: default;
&.interactive {
cursor: pointer;
&:hover {
background-color: rgba($white, 0.75);
}
}
.icon {
color: $gray-10;
&.lock {
width: 10px;
}
&.check {
margin-left: 2px;
width: 12px;
}
}
}
}
.info {
width: 100%;
border-top: 1px solid $gray-600;
}
}
.claim-color {
color: $blue-10 !important;
.gray-100 {
color: $gray-100;
}
.unclaim-color {
color: $red-50 !important;
.green-10 {
color: $green-10;
}
.last-completed {
width: 16px;
height: 14px;
margin-bottom: 4px;
}
.small-check {
display: inline-flex;
width: 16px;
height: 16px;
border-radius: 2px;
background-color: $gray-500;
.svg-icon {
width: 8px;
height: 6px;
margin: auto;
}
}
.users-icon {
width: 16px;
}
</style>
<script>
import findIndex from 'lodash/findIndex';
import moment from 'moment';
import reduce from 'lodash/reduce';
import { mapState } from '@/libs/store';
import approvalModal from './approvalModal';
import sync from '@/mixins/sync';
import checkIcon from '@/assets/svg/check.svg';
import lockIcon from '@/assets/svg/lock.svg';
import usersIcon from '@/assets/svg/users.svg';
import lastComplete from '@/assets/svg/last-complete.svg';
export default {
components: {
approvalModal,
},
mixins: [sync],
props: ['task', 'group'],
data () {
return {
showStatus: false,
icons: Object.freeze({
check: checkIcon,
lastComplete,
lock: lockIcon,
users: usersIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
userIsAssigned () {
return this.task.group.assignedUsers
&& this.task.group.assignedUsers.indexOf(this.user._id) !== -1;
},
message () {
const { assignedUsers } = this.task.group;
const assignedUsersNames = [];
const assignedUsersLength = assignedUsers.length;
// @TODO: Eh, I think we only ever display one user name
if (this.group && this.group.members) {
assignedUsers.forEach(userId => {
const index = findIndex(this.group.members, member => member._id === userId);
const assignedMember = this.group.members[index];
assignedUsersNames.push(`@${assignedMember.auth.local.username}`);
});
}
if (assignedUsersLength === 1 && !this.userIsAssigned) {
return this.$t('assignedToUser', { userName: assignedUsersNames[0] });
} if (assignedUsersLength > 1 && !this.userIsAssigned) {
return this.$t('assignedToMembers', { userCount: assignedUsersLength });
} if (assignedUsersLength > 1 && this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: assignedUsersLength - 1 });
} if (this.userIsAssigned) {
return this.$t('youAreAssigned');
} // if (assignedUsersLength === 0) {
return this.$t('taskIsUnassigned');
return this.task.group.assignedUsersDetail
&& Boolean(this.task.group.assignedUsersDetail[this.user._id]);
},
userIsManager () {
if (
this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id])
) return true;
return false;
return this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id]);
},
approvalRequested () {
if (
(this.task.approvals && this.task.approvals.length === 1)
|| (this.task.group && this.task.group.approval && this.task.group.approval.requested)
) {
return true;
assignedUsersCount () {
return this.task.group.assignedUsers.length;
},
completionsCount () {
return reduce(this.task.group.assignedUsersDetail, (count, assignment) => {
if (assignment.completed) return count + 1;
return count;
}, 0);
},
completionsList () {
if (this.assignedUsersCount === 1) return [];
const completionsArray = [];
for (const userId of this.task.group.assignedUsers) {
if (userId !== this.user._id) {
const { completedDate } = this.task.group.assignedUsersDetail[userId];
let completedDateString;
if (moment().diff(completedDate, 'days') > 0) {
completedDateString = `Completed ${moment(completedDate).format('M/D/YY')}`;
} else {
completedDateString = `Completed at ${moment(completedDate).format('h:mm A')}`;
}
completionsArray.push({
userId,
userName: this.task.group.assignedUsersDetail[userId].assignedUsername,
completed: this.task.group.assignedUsersDetail[userId].completed,
completedDate,
completedDateString,
});
}
}
return false;
return completionsArray;
},
multipleApprovalsRequested () {
if (this.task.approvals && this.task.approvals.length > 1) return true;
return false;
message () {
if (this.assignedUsersCount > 1) { // Multi assigned
if (this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: this.assignedUsersCount - 1 });
}
return this.$t('assignedToMembers', { userCount: this.assignedUsersCount });
}
if (this.assignedUsersCount === 1) { // Singly assigned
const userId = this.task.group.assignedUsers[0];
const userName = this.task.group.assignedUsersDetail[userId].assignedUsername;
if (this.task.group.assignedUsersDetail[userId].completed) { // completed
const { completedDate } = this.task.group.assignedUsersDetail[userId];
if (this.userIsAssigned) {
if (moment().diff(completedDate, 'days') > 0) {
return `<strong>You</strong> completed ${moment(completedDate).format('M/D/YY')}`;
}
return `<strong>You</strong> completed at ${moment(completedDate).format('h:mm A')}`;
}
if (moment().diff(completedDate, 'days') > 0) {
return `@${userName} completed ${moment(completedDate).format('M/D/YY')}`;
}
return `@${userName} completed at ${moment(completedDate).format('h:mm A')}`;
}
if (this.userIsAssigned) {
return this.$t('youEmphasized');
}
return `@${userName}`;
}
return this.$t('error'); // task is open, or the other conditions aren't hitting right
},
singleAssignLastDone () {
const completion = this.task?.group?.assignedUsersDetail[this.user._id];
if (completion) return completion.completedDate;
return null;
},
formattedCompletionTime () {
if (!this.singleAssignLastDone) return '';
if (moment().diff(this.singleAssignLastDone, 'days') < 1) {
return moment(this.singleAssignLastDone).format('h:mm A');
}
return moment(this.singleAssignLastDone).format('M/D/YY');
},
},
methods: {
async claim () {
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:assignTask', {
taskId,
userId: this.user._id,
});
this.task.group.assignedUsers.push(this.user._id);
this.sync();
iconClass (completion) {
if (this.userIsManager && completion.completed) return 'check';
return 'lock';
},
async unassign () {
if (!window.confirm(this.$t('confirmUnClaim'))) return; // eslint-disable-line no-alert
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:unassignTask', {
taskId,
userId: this.user._id,
});
const index = this.task.group.assignedUsers.indexOf(this.user._id);
this.task.group.assignedUsers.splice(index, 1);
this.sync();
},
approve () {
const userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(0, 1);
this.task.approvals.splice(0, 1);
this.sync();
},
needsWork () {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[0];
needsWork (completion) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
userId: completion.userId,
});
this.task.approvals.splice(0, 1);
this.sync();
},
showRequests () {
this.$root.$emit('bv::show::modal', 'approval-modal');
this.task.group.assignedUsersDetail[completion.userId].completed = false;
},
},
};
@@ -1,80 +0,0 @@
<template>
<b-modal
id="approval-modal"
:title="$t('approveTask')"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
v-for="(approval, index) in task.approvals"
class="row approval"
>
<div class="col-8">
<strong>{{ approval.userId.profile.name }}</strong>
</div>
<div class="col-2">
<button
class="btn btn-primary"
@click="approve(index)"
>
{{ $t('approve') }}
</button>
</div>
<div class="col-2">
<button
class="btn btn-secondary"
@click="needsWork(index)"
>
{{ $t('needsWork') }}
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('close') }}
</button>
</div>
</b-modal>
</template>
<style scoped>
.row.approval {
padding-top: 1em;
padding-bottom: 1em;
}
</style>
<script>
export default {
props: ['task'],
methods: {
approve (index) {
const userIdToApprove = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(index, 1);
this.task.approvals.splice(index, 1);
},
needsWork (index) {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(index, 1);
},
close () {
this.$root.$emit('bv::hide::modal', 'approval-modal');
},
},
};
</script>
+38 -15
View File
@@ -43,7 +43,7 @@
class="tasks-list"
>
<textarea
v-if="isUser"
v-if="isUser || canCreateTasks()"
ref="quickAdd"
v-model="quickAddText"
class="quick-add"
@@ -102,6 +102,7 @@
:group="group"
:challenge="challenge"
@editTask="editTask"
@taskSummary="taskSummary"
@moveTo="moveTo"
@taskDestroyed="taskDestroyed"
/>
@@ -347,18 +348,19 @@
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';
import ClearCompletedTodos from './clearCompletedTodos';
import buyMixin from '@/mixins/buy';
import sync from '@/mixins/sync';
import { mapState, mapActions, mapGetters } from '@/libs/store';
import shopItem from '../shops/shopItem';
import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue';
import PinBadge from '@/components/ui/pinBadge';
import notifications from '@/mixins/notifications';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import {
getTypeLabel,
@@ -382,7 +384,7 @@ export default {
shopItem,
draggable,
},
mixins: [buyMixin, notifications],
mixins: [buyMixin, notifications, sync],
// @TODO Set default values for props
// allows for better control of props values
// allows for better control of where this component is called
@@ -542,6 +544,7 @@ export default {
...mapActions({
loadCompletedTodos: 'tasks:fetchCompletedTodos',
createTask: 'tasks:create',
createGroupTasks: 'tasks:createGroupTasks',
}),
async taskSorted (data) {
const filteredList = this.taskList;
@@ -574,18 +577,25 @@ export default {
},
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
const list = this.getUnfilteredTaskList(this.type);
const list = this.taskListOverride || this.getUnfilteredTaskList(this.type);
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
const moved = list.splice(oldPosition, 1);
const newPosition = where === 'top' ? 0 : list.length;
list.splice(newPosition, 0, moved[0]);
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
if (!this.isUser) {
await this.$store.dispatch('tasks:moveGroupTask', {
taskId: taskIdToMove,
position: newPosition,
});
} else {
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
}
},
async rewardSorted (data) {
const rewardsList = this.inAppRewards;
@@ -606,7 +616,12 @@ export default {
this.showPopovers = true;
this.isDragging(false);
},
quickAdd (ev) {
canCreateTasks () {
if (!this.group) return false;
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
async quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
if (ev.shiftKey) {
this.quickAddRows += 1;
@@ -620,19 +635,27 @@ export default {
const tasks = text.split('\n').reverse().filter(taskText => (!!taskText)).map(taskText => {
const task = taskDefaults({ type: this.type, text: taskText }, this.user);
task.tags = this.selectedTags.slice();
if (this.isUser) task.tags = this.selectedTags.slice();
return task;
});
this.quickAddText = '';
this.quickAddRows = 1;
this.createTask(tasks);
if (this.group) {
await this.createGroupTasks({ groupId: this.group.id, tasks });
this.sync();
} else {
this.createTask(tasks);
}
this.$refs.quickAdd.blur();
return true;
},
editTask (task) {
this.$emit('editTask', task);
},
taskSummary (task) {
this.$emit('taskSummary', task);
},
activateFilter (type, filter = '') {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
@@ -687,7 +710,7 @@ export default {
filterByLabel (taskList, type, filter) {
if (!taskList) return [];
const selectedFilter = getActiveFilter(type, filter, this.challenge);
return sortAndFilterTasks(taskList, selectedFilter);
return sortAndFilterTasks(taskList, selectedFilter, Boolean(this.group));
},
filterByTagList (taskList, tagList = []) {
let filteredTaskList = taskList;
@@ -1,80 +1,98 @@
<template>
<div class="checklist-component">
<lockable-label
:locked="disabled || disableItems"
:text="$t('checklist')"
/>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
<div
class="d-flex"
>
<lockable-label
:locked="disabled"
:text="$t('checklist')"
/>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
class="svg-icon icon-16 my-auto ml-auto pointer"
:class="{'chevron-flip': showChecklist}"
v-html="icons.chevron"
@click="showChecklist = !showChecklist"
>
</div>
</div>
<b-collapse
id="checklistCollapse"
v-model="showChecklist"
>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
>
</span>
<checkbox
v-if="!disableEdit"
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled"
class="input-group-prepend"
:class="{'cursor-auto': disabled}"
/>
<input
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableEdit"
:class="summaryClass(item)"
>
<span
v-if="!disabled && !disableEdit"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableEdit"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<checkbox
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled || disableItems"
class="input-group-prepend"
:class="{'cursor-auto': disabled || disableItems}"
/>
<input
v-model="item.text"
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableItems"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
<span
v-if="!disabled && !disableItems"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableItems"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<input
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
</div>
</b-collapse>
</div>
</template>
@@ -104,7 +122,7 @@ export default {
disableDrag: {
type: Boolean,
},
disableItems: {
disableEdit: {
type: Boolean,
},
items: {
@@ -114,6 +132,7 @@ export default {
data () {
return {
checklist: this.items,
showChecklist: true,
hasPossibilityOfIMEConversion: true,
newChecklistItem: null,
icons: Object.freeze({
@@ -125,6 +144,11 @@ export default {
};
},
methods: {
summaryClass (item) {
if (!this.disableEdit) return '';
if (item.completed) return 'summary-completed';
return 'summary-incomplete';
},
updateChecklist () {
this.$emit('update:items', this.checklist);
},
@@ -166,14 +190,22 @@ export default {
.checklist-component {
.top-border {
border-top: 1px solid $gray-500;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
}
.lock-icon {
color: $gray-200;
}
.pointer {
cursor: pointer;
}
.top-border {
border-top: 1px solid $gray-500;
}
.checklist-group {
height: 2rem;
border-bottom: 1px solid $gray-500;
@@ -251,6 +283,13 @@ export default {
border-radius: 0px;
border: none !important;
padding-left: 36px;
&.summary-incomplete {
opacity: 1;
}
&.summary-completed {
text-decoration: line-through;
}
}
.checklist-group {
@@ -4,16 +4,16 @@
:class="{ 'break': maxItems === 0 }"
>
<template v-if="items.length === 0">
<div class="items-none">
<div class="items-none mb-1">
{{ emptyMessage }}
</div>
</template>
<template v-else>
<div
v-for="item in truncatedSelectedItems"
v-for="item in items"
:key="item.id"
:title="item.name"
class="multi-item mr-1 d-inline-flex align-items-center"
class="multi-item mr-1 mb-1 d-inline-flex align-items-center"
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
@click.stop="removeItem($event, item)"
@@ -27,12 +27,6 @@
v-html="icons.remove"
></div>
</div>
<div
v-if="remainingSelectedItems.length > 0"
class="items-more ml-75"
>
+{{ remainingSelectedItems.length }}
</div>
</template>
</div>
</template>
@@ -1,4 +1,4 @@
multi<template>
<template>
<div>
<b-dropdown
ref="dropdown"
@@ -29,6 +29,7 @@ multi<template>
</b-dropdown-header>
<template v-slot:button-content>
<multi-list
class="d-flex flex-wrap"
:items="selectedItemsAsObjects"
:add-new="addNew"
:pill-invert="pillInvert"
@@ -85,6 +86,13 @@ multi<template>
$itemHeight: 2rem;
.inline-dropdown {
&.select-multi .dropdown-toggle {
height: auto;
padding-bottom: 0px;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
@@ -0,0 +1,294 @@
<template>
<div>
<b-dropdown
ref="dropdown"
class="inline-dropdown select-multi"
:toggle-class="isOpened ? 'active' : null"
:class="{'margin-adjust': selectedItem}"
@show="wasOpened()"
@hide="hideCallback($event)"
@toggle="openOrClose($event)"
>
<b-dropdown-header>
<div class="mb-2">
<b-form-input
v-model="search"
type="text"
:placeholder="searchPlaceholder"
@keyup.enter="handleSubmit"
/>
</div>
</b-dropdown-header>
<template v-slot:button-content>
<div
class="mr-1 d-inline-flex align-items-center"
@click.stop="selectItem({id: selectedItem})"
v-markdown="
allItemsMap[selectedItem] ? `@${allItemsMap[selectedItem].name}`
: emptyMessage
"
>
</div>
</template>
<div
v-if="addNew || availableToSelect.length > 0"
:class="{
'item-group': true,
'add-new': availableToSelect.length === 0 && search !== '',
'scroll': availableToSelect.length > 5
}"
>
<b-dropdown-item-button
v-for="item in availableToSelect"
:key="item.id"
class="ignore-hide multi-item"
:class="{ 'none': item.id === 'none', selectListItem: true }"
@click.prevent.stop="selectItem(item)"
>
<div
v-markdown="item.name"
class="label"
></div>
<div
v-if="item.addlText"
class="addl-text"
>
{{ item.addlText }}
</div>
</b-dropdown-item-button>
</div>
</b-dropdown>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
$itemHeight: 2rem;
.selected-item {
display: inline-block;
height: 1.5rem;
border-radius: 100px;
background-color: $white;
border: solid 1px $gray-400;
position: relative;
top: -1px;
.multi-label {
height: 1rem;
font-size: 12px;
line-height: 16px;
letter-spacing: normal;
color: $gray-100;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
}
.dropdown-header {
background-color: $gray-700;
padding-bottom: 0;
min-height: 3rem;
}
.dropdown-item, .dropdown-header {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.none {
cursor: default;
pointer-events: none;
}
.multi-item button {
height: $itemHeight;
display: flex;
.label {
height: 1.5rem;
font-size: 14px;
line-height: 1.71;
flex: 1;
}
.addl-text {
height: 1rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
text-align: right;
color: $gray-100;
align-self: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
margin-right: 0.25rem;
}
&:hover {
.addl-text {
color: $purple-300;
}
}
}
.item-group {
max-height: #{5*$itemHeight};
&.add-new {
height: 30px;
.hint {
display: block;
}
}
&.scroll {
overflow-y: scroll;
}
}
.hint {
display: none;
height: 2rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
color: $gray-100;
margin-left: 0.75rem;
margin-top: 0.5rem;
}
}
</style>
<script>
import Vue from 'vue';
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
components: {
},
props: {
addNew: {
type: Boolean,
default: false,
},
allItems: {
type: Array,
},
emptyMessage: {
type: String,
},
searchPlaceholder: {
type: String,
},
selectedItem: {
type: String,
},
},
data () {
return {
preventHide: true,
isOpened: false,
selected: this.selectedItem,
search: '',
};
},
computed: {
allItemsMap () {
const obj = {};
this.allItems.forEach(t => {
obj[t.id] = t;
});
return obj;
},
selectedItemAsObject () {
return this.selectedItem ? this.allItemsMap[this.selectedItem] : null;
},
availableToSelect () {
const searchString = this.search.toLowerCase();
const filteredItems = this.allItems.filter(i => i.name.toLowerCase().includes(searchString));
return filteredItems;
},
},
watch: {
selected () {
this.$emit('changed', this.selectedItem);
},
},
created () {
document.addEventListener('keyup', this.handleEsc);
},
beforeDestroy () {
document.removeEventListener('keyup', this.handleEsc);
},
mounted () {
this.$refs.dropdown.clickOutHandler = () => {
this.closeSelectPopup();
};
},
methods: {
closeSelectPopup () {
this.preventHide = false;
this.isOpened = false;
Vue.nextTick(() => {
this.$refs.dropdown.hide();
});
},
openOrClose ($event) {
if (this.isOpened) {
this.closeSelectPopup();
$event.preventDefault();
}
},
selectItem (item) {
if (item.id === this.selectedItem) {
this.$emit('toggle', null);
} else {
this.$emit('toggle', item.id);
}
this.closeSelectPopup();
},
hideCallback ($event) {
if (this.preventHide) {
$event.preventDefault();
return;
}
this.isOpened = false;
},
wasOpened () {
this.isOpened = true;
this.preventHide = true;
},
handleEsc (e) {
if (e.keyCode === 27) {
this.closeSelectPopup();
}
},
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
this.$emit('addNew', search);
this.search = '';
},
},
};
</script>
@@ -9,6 +9,7 @@
:right="right"
:hide-icon="false"
:inline-dropdown="inlineDropdown"
:placeholder="placeholder"
@select="selectItem($event)"
>
<template v-slot:item="{ item }">
@@ -70,6 +71,9 @@ export default {
type: Boolean,
default: true,
},
placeholder: {
type: String,
},
},
data () {
return {
+15 -17
View File
@@ -68,7 +68,8 @@
</b-popover>
<div
class="spell-border"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl }"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl,
'insufficient-mana': user.stats.mp < skill.mana }"
>
<div
class="spell"
@@ -87,19 +88,6 @@
<div>Level {{ skill.lvl }}</div>
</div>
</div>
<div
v-else-if="spellDisabled(key) === true"
class="mana"
>
<div class="mana-text">
<div
v-once
class="svg-icon"
v-html="icons.mana"
></div>
<div>{{ skill.mana }}</div>
</div>
</div>
<div
v-else
class="mana"
@@ -200,7 +188,7 @@
border-radius: 4px;
margin-bottom: 1rem;
&:hover:not(.disabled) {
&:hover:not(.disabled):not(.insufficient-mana) {
background-color: $purple-400;
cursor: pointer;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16),
@@ -216,6 +204,10 @@
background-color: rgba(26, 24, 29, 0.5);
}
.mana-text {
color: $blue-500;
}
.level {
color: $white;
font-weight: normal;
@@ -223,6 +215,10 @@
}
}
&.insufficient-mana:not(.disabled) {
opacity: 0.5;
}
.spell {
background: $white;
border-radius: 4px;
@@ -465,7 +461,7 @@ export default {
spellDisabled (skill) {
const incompleteDailiesDue = this
.getUnfilteredTaskList('daily')
.filter(daily => !daily.completed && daily.isDue)
.filter(daily => !daily.completed && !daily.group.id && daily.isDue)
.length;
if (skill === 'frost' && this.user.stats.buffs.streaks) {
@@ -481,7 +477,9 @@ export default {
skillNotes (skill) {
let notes = skill.notes();
if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
if (this.user.stats.lvl < skill.lvl) {
notes = this.$t('spellLevelTooHigh', { level: skill.lvl });
} else if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
} else if (skill.key === 'stealth' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
+139 -84
View File
@@ -3,47 +3,39 @@
<div
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess},
`type_${task.type}`
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
>
<approval-header
v-if="task.group.id"
:task="task"
:group="group"
/>
<div
class="d-flex"
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
&& !(task.group.approval.approved && task.type === 'habit')}"
:class="{'task-not-scoreable': showTaskLockIcon }"
>
<!-- Habits left side control-->
<div
v-if="task.type === 'habit'"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass
}, controlClass.up.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-positive-enabled': task.up && isUser,
'habit-control-positive-disabled': !task.up && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.up.inner]"
tabindex="0"
@click="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@click="score('up')"
@keypress.enter="score('up')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.up ? controlClass.up.icon : 'positive'"
v-html="icons.lock"
@@ -60,20 +52,21 @@
v-if="task.type === 'daily' || task.type === 'todo'"
class="left-control d-flex justify-content-center"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.bg]"
>
<div
class="task-control daily-todo-control"
:class="controlClass.inner"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.inner,
]"
tabindex="0"
@click="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@keypress.enter="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="controlClass.icon"
v-html="icons.lock"
@@ -82,7 +75,7 @@
v-else
class="svg-icon check"
:class="{
'display-check-icon': task.completed || task.group.approval.requested,
'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
@@ -95,8 +88,8 @@
:class="contentClass"
>
<div
class="task-clickable-area"
:class="{ 'cursor-auto': !isUser && !teamManagerAccess }"
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
@@ -105,14 +98,14 @@
<h3
v-markdown="task.text"
class="task-title markdown"
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
:class="{ 'has-notes': task.notes }"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
ref="taskDropdown"
v-b-tooltip.hover.top="$t('options')"
tabindex="0"
class="task-dropdown"
class="task-dropdown mr-1"
:right="task.type === 'reward'"
>
<div slot="dropdown-toggle">
@@ -138,7 +131,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToTop"
@@ -153,7 +145,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToBottom"
@@ -186,7 +177,7 @@
</menu-dropdown>
</div>
<div
v-markdown="displayNotes"
v-markdown="task.notes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
@@ -220,7 +211,7 @@
v-if="!task.collapseChecklist"
:key="item.id"
class="custom-control custom-checkbox checklist-item"
:class="{'checklist-item-done': item.completed, 'cursor-auto': !isUser}"
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<input
@@ -229,14 +220,14 @@
tabindex="0"
type="checkbox"
:checked="item.completed"
:disabled="castingSpell || !isUser"
:disabled="castingSpell || showTaskLockIcon"
@change="toggleChecklistItem(item)"
@keypress.enter="toggleChecklistItem(item)"
>
<label
v-markdown="item.text"
class="custom-control-label"
:class="{ 'cursor-auto': !isUser }"
:class="{ 'cursor-auto': showTaskLockIcon }"
:for="`checklist-${item.id}-${random}`"
></label>
</div>
@@ -316,7 +307,7 @@
></div>
</div>
<div
v-if="hasTags"
v-if="hasTags && !task.group.id"
:id="`tags-icon-${task._id}`"
class="d-flex align-items-center"
>
@@ -326,7 +317,7 @@
></div>
</div>
<b-popover
v-if="hasTags"
v-if="hasTags && !task.group.id"
:target="`tags-icon-${task._id}`"
triggers="hover"
placement="bottom"
@@ -356,25 +347,22 @@
v-if="task.type === 'habit'"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.down.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-negative-enabled': task.down && isUser,
'habit-control-negative-disabled': !task.down && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.down.inner]"
tabindex="0"
@click="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.down ? controlClass.down.icon : 'negative'"
v-html="icons.lock"
@@ -390,13 +378,22 @@
<div
v-if="task.type === 'reward'"
class="right-control d-flex align-items-center justify-content-center reward-control"
:class="controlClass.bg"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.bg,
]"
tabindex="0"
@click="isUser ? score('down') : null"
@keypress.enter="isUser ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
class="svg-icon"
v-if="showTaskLockIcon"
class="svg-icon color lock"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon mb-1"
v-html="icons.gold"
></div>
<div class="small-text">
@@ -405,10 +402,9 @@
</div>
</div>
<approval-footer
v-if="task.group.id"
v-if="task.group.id && !isOpenTask"
:task="task"
:group="group"
@claimRewards="score('up')"
/>
</div>
</div>
@@ -427,7 +423,7 @@
border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 2px;
border-radius: 4px;
}
}
@@ -449,11 +445,11 @@
margin-bottom: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: white;
border-radius: 2px;
border-radius: 4px;
position: relative;
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
@@ -469,10 +465,10 @@
}
.task.groupTask {
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
border: $purple-400 solid 1px;
border-radius: 3px;
border-radius: 5px;
margin: -1px; // to counter the border width
margin-bottom: 1px;
transition: none; // with transition, the border color switches from black to $purple-400
@@ -495,6 +491,7 @@
}
&.has-notes {
margin-bottom: 0px;
padding-bottom: 4px;
}
@@ -508,8 +505,6 @@
}
.task-clickable-area {
padding: 7px 8px;
padding-bottom: 0px;
border: transparent solid 1px;
cursor: pointer;
@@ -518,7 +513,7 @@
}
&:focus {
border-radius: 2px;
border-radius: 4px;
border: $purple-400 solid 1px;
}
}
@@ -575,7 +570,11 @@
}
.task-dropdown {
max-height: 18px;
height: 16px;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.task-dropdown ::v-deep .dropdown-menu {
@@ -632,8 +631,8 @@
}
&.reward-content {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
@@ -796,8 +795,8 @@
}
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
min-height: 60px;
border: 1px solid transparent;
border-right: none;
@@ -809,15 +808,15 @@
.task:not(.type_habit) {
.left-control {
& + .task-content {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
min-height: 56px;
border: 1px solid transparent;
border-left: none;
@@ -853,8 +852,12 @@
height: 24px;
}
.lock {
color: $gray-200;
width: 15px;
}
.small-text {
margin-top: 4px;
font-style: initial;
font-weight: bold;
}
@@ -920,20 +923,19 @@ import lockIcon from '@/assets/svg/lock.svg';
import menuIcon from '@/assets/svg/menu.svg';
import markdownDirective from '@/directives/markdown';
import scoreTask from '@/mixins/scoreTask';
import approvalHeader from './approvalHeader';
import sync from '@/mixins/sync';
import approvalFooter from './approvalFooter';
import MenuDropdown from '../ui/customMenuDropdown';
export default {
components: {
approvalFooter,
approvalHeader,
MenuDropdown,
},
directives: {
markdown: markdownDirective,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
// @TODO: maybe we should store the group on state?
props: {
task: {},
@@ -1064,11 +1066,45 @@ export default {
},
teamManagerAccess () {
if (!this.isGroupTask || !this.group) return true;
if (!this.group.leader && !this.group.managers) return false;
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
},
displayNotes () {
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
return this.task.notes;
isOpenTask () {
if (!this.isGroupTask) return false;
if (this.task?.group?.assignedUsers?.length > 0) return false;
return true;
},
showCheckIcon () {
if (this.isGroupTask && this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]) {
return this.task.group.assignedUsersDetail[this.user._id].completed;
}
return this.task.completed;
},
showTaskLockIcon () {
if (this.isUser) return false;
if (this.isGroupTask) {
if (this.task.completed) {
if (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]
) {
return false;
}
if (this.task.group.completedBy.userId === this.user._id) return false;
if (this.teamManagerAccess) {
if (!this.task.group.assignedUsers || this.task.group.assignedUsers.length === 0) {
return false;
}
if (this.task.group.assignedUsers.length === 1) return false;
}
return true;
}
if (this.isOpenTask) return false;
if (this.task.group.assignedUsersDetail[this.user._id]) {
return false;
}
}
return true;
},
},
methods: {
@@ -1098,7 +1134,7 @@ export default {
return this.task.date && this.$t('dueIn', { dueIn });
},
edit (e, task) {
if (this.isRunningYesterdailies || !this.showEdit) return;
if (this.isRunningYesterdailies) return;
const target = e.target || e.srcElement;
/*
@@ -1116,8 +1152,11 @@ export default {
const isEditAction = this.$refs.editTaskItem && this.$refs.editTaskItem.contains(target);
if (isDropdown && !isEditAction) return;
if (this.$store.state.spellOptions.castingSpell) return;
if (!this.$store.state.spellOptions.castingSpell) {
if (!this.showEdit) {
this.$emit('taskSummary', task);
} else {
this.$emit('editTask', task);
}
},
@@ -1137,6 +1176,22 @@ export default {
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
},
async score (direction) {
if (this.showTaskLockIcon) return;
if (this.task.type === 'habit' && !this.task[direction]) return;
if (
this.isGroupTask && direction === 'down'
&& ['todo', 'daily'].indexOf(this.task.type) !== -1
&& !((this.task.group.completedBy && this.task.group.completedBy.userId === this.user._id)
|| (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]))
) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: this.task.group.assignedUsers[0] || this.task.group.completedBy.userId,
});
this.task.completed = false;
return;
}
if (this.isYesterdaily === true) {
await this.beforeTaskScore(this.task);
this.task.completed = !this.task.completed;
+76 -328
View File
@@ -6,7 +6,7 @@
size="sm"
:hide-footer="true"
@hidden="onClose()"
@show="handleOpen()"
@show="syncTask()"
@shown="focusInput()"
>
<div
@@ -22,7 +22,9 @@
>
{{ title }}
</h2>
<div class="ml-auto d-flex align-items-center">
<div
class="ml-auto d-flex align-items-center"
>
<button
class="cancel-task-btn mr-3"
:class="cssClass('headings')"
@@ -55,7 +57,7 @@
<div class="form-group">
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<input
@@ -66,28 +68,29 @@
type="text"
required="required"
spellcheck="true"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
>
</div>
<div
v-if="isUserTask || isChallengeTask || isOriginalChallengeTask"
class="form-group mb-0"
>
<label
class="d-flex align-items-center justify-content-between mb-1"
>
<span
:class="cssClass('headings')"
>{{ $t('notes') }}</span>
<small>
<div class="d-flex">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
:text="`${$t('notes')}`"
/>
<small
class="my-1"
>
<a
target="_blank"
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
:class="cssClass('headings')"
>{{ $t('markdownHelpLink') }}</a>
</small>
</label>
</div>
<textarea
v-model="task.notes"
class="form-control input-notes"
@@ -95,49 +98,6 @@
:placeholder="$t('addNotes')"
></textarea>
</div>
<div
v-if="showManagerNotes"
class="form-group mb-0 mt-3"
>
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage"
:text="$t('managerNotes')"
/>
<textarea
v-model="managerNotes"
class="form-control input-notes"
:class="cssClass('input')"
:placeholder="$t('addNotes')"
:disabled="groupAccessRequiredAndOnPersonalPage"
></textarea>
</div>
<div
v-if="task.group && task.group.assignedDate && !task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateOnly', {
date: formattedDate(task.group.assignedDate),
})"
>
</div>
<div
v-if="task.group && task.group.assignedDate && task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateAndUser', {
username: task.group.assigningUsername,
date: formattedDate(task.group.assignedDate),
})"
>
</div>
</div>
<div
v-if="task && groupAccessRequiredAndOnPersonalPage
&& (task.type === 'daily' || task.type === 'todo')"
class="summary-sentence py-3 px-4"
v-html="summarySentence"
>
</div>
<div
v-if="task"
@@ -181,8 +141,6 @@
>
<checklist
:items.sync="task.checklist"
:disable-items="groupAccessRequiredAndOnPersonalPage"
:disable-drag="groupAccessRequiredAndOnPersonalPage"
/>
</div>
<div
@@ -194,7 +152,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.up ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleUpDirection()"
>
<div
@@ -220,7 +178,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.down ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleDownDirection()"
>
<div
@@ -243,11 +201,11 @@
</button>
</div>
<template
v-if="task.type !== 'reward' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type !== 'reward'"
>
<div class="d-flex mt-3">
<lockable-label
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="$t('difficulty')"
/>
<div
@@ -258,13 +216,12 @@
</div>
<select-difficulty
:value="task.priority"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
@select="setDifficulty($event)"
/>
</template>
<div
v-if="task.type === 'todo' && !groupAccessRequiredAndOnPersonalPage
&& (!challengeAccessRequired || task.date)"
v-if="task.type === 'todo' && (!challengeAccessRequired || task.date)"
class="option mt-3"
>
<div class="form-group">
@@ -281,7 +238,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -297,7 +254,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -314,7 +271,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -344,8 +301,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && task.frequency === 'weekly'
&& !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily' && task.frequency === 'weekly'"
class="option mt-3"
>
<div class="form-group">
@@ -405,7 +361,7 @@
</div>
</div>
<div
v-if="isUserTask"
v-if="!groupId"
class="tags-select option mt-3"
>
<div class="tags-inline form-group row">
@@ -428,16 +384,16 @@
</div>
</div>
<div
v-if="task.type === 'habit'"
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
>
<div class="form-group">
<lockable-label
:locked="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:locked="challengeAccessRequired"
:text="$t('resetCounter')"
/>
<select-translated-array
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
:items="['daily', 'weekly', 'monthly']"
:value="task.frequency"
@select="task.frequency = $event"
@@ -448,25 +404,18 @@
v-if="groupId"
class="option group-options mt-3"
>
<div
v-if="task.type === 'todo'"
class="form-group"
>
<label
v-once
class="mb-1"
>{{ $t('sharedCompletion') }}</label>
<select-translated-array
:items="['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']"
:value="sharedCompletion"
@select="sharedCompletion = $event"
/>
</div>
<div class="form-group row mt-3 mb-3">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedTo') }}</label>
class="col-10 mb-1"
>{{ $t('assignTo') }}</label>
<a
v-if="assignedMembers.length > 0"
class="col-2 text-right mt-1"
@click="clearAssignments"
>
{{ $t('clear') }}
</a>
<div class="col-12">
<select-multi
ref="assignMembers"
@@ -479,17 +428,6 @@
/>
</div>
</div>
<div class="form-group flex-group mt-3 mb-4">
<label
v-once
class="mb-0 flex"
>{{ $t('approvalRequired') }}</label>
<toggle-switch
class="d-inline-block"
:checked="requiresApproval"
@change="updateRequiresApproval"
/>
</div>
</div>
<div
v-if="advancedSettingsAvailable"
@@ -605,9 +543,7 @@
</b-collapse>
</div>
<div
v-if="purpose !== 'create'
&& !challengeAccessRequired
&& !groupAccessRequiredAndOnPersonalPage"
v-if="purpose !== 'create' && !challengeAccessRequired"
class="d-flex justify-content-center mt-4 mb-4"
>
<button
@@ -654,6 +590,12 @@
@import '~@/assets/scss/colors.scss';
#task-modal {
a:not(.dropdown-item) {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
}
.modal-dialog.modal-sm {
max-width: 448px;
}
@@ -686,9 +628,6 @@
}
input, textarea {
&:not(:host-context(.tags-popup)) {
border: none;
}
transition-property: border-color, box-shadow, color, background;
background-color: rgba(255, 255, 255, 0.5);
&:focus:not(:disabled), &:active:not(:disabled), &:hover:not(:disabled) {
@@ -1026,11 +965,6 @@
}
}
.summary-sentence {
background-color: $gray-700;
line-height: 1.71;
}
.input-group-text {
font-size: 14px;
font-weight: bold;
@@ -1051,12 +985,8 @@
<script>
import axios from 'axios';
import clone from 'lodash/clone';
import keys from 'lodash/keys';
import pickBy from 'lodash/pickBy';
import moment from 'moment';
import Datepicker from '@/components/ui/datepicker';
import toggleSwitch from '@/components/ui/toggleSwitch';
import toggleCheckbox from '@/components/ui/toggleCheckbox';
import markdownDirective from '@/directives/markdown';
import { mapGetters, mapActions, mapState } from '@/libs/store';
@@ -1066,6 +996,8 @@ import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
@@ -1076,11 +1008,11 @@ import chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg';
export default {
components: {
SelectMulti,
Datepicker,
toggleSwitch,
checklist,
selectDifficulty,
selectTranslatedArray,
@@ -1090,6 +1022,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [syncTask],
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {
@@ -1107,9 +1040,6 @@ export default {
calendar: calendarIcon,
grip: gripIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
sharedCompletion: 'singleCompletion',
managerNotes: '',
members: [],
membersNameAndId: [],
memberNamesById: {},
@@ -1123,15 +1053,6 @@ export default {
per: 'perception',
},
calendarHighlights: { dates: [new Date()] },
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
};
},
computed: {
@@ -1150,7 +1071,6 @@ export default {
|| this.task.type === 'todo'
|| this.purpose === 'create'
|| !this.isUserTask
|| this.groupAccessRequiredAndOnPersonalPage
) {
return false;
}
@@ -1164,29 +1084,17 @@ export default {
return true;
},
groupAccessRequiredAndOnPersonalPage () {
if (!this.groupId && this.task.group && this.task.group.id) return true;
return false;
},
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1
&& !this.isOriginalChallengeTask
&& (!this.groupAccessRequiredAndOnPersonalPage || this.checklist.length > 0);
},
showManagerNotes () {
return Boolean(this.task.group && this.task.group.managerNotes)
|| (
!this.groupAccessRequiredAndOnPersonalPage && this.managers.indexOf(this.user._id) !== -1
);
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
isChallengeTask () {
return Boolean(this.task.challenge && this.task.challenge.id);
},
onUserPage () {
isUserTask () {
return !this.challengeId && !this.groupId;
},
challengeAccessRequired () {
return this.onUserPage && this.isChallengeTask;
return this.isUserTask && this.isChallengeTask;
},
isOriginalChallengeTask () {
const isUserChallenge = Boolean(this.task.userId);
@@ -1203,9 +1111,6 @@ export default {
const type = this.$t(this.task.type);
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type });
},
isUserTask () {
return !this.challengeId && !this.groupId;
},
repeatSuffix () {
const { task } = this;
@@ -1240,23 +1145,6 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
},
watch: {
task () {
@@ -1284,47 +1172,6 @@ export default {
createTask: 'tasks:create',
createTag: 'tags:createTag',
}),
async syncTask () {
if (this.task?.group?.managerNotes) {
this.managerNotes = this.task.group.managerNotes;
}
if (this.groupId && this.task.group?.approval) {
this.requiresApproval = this.task.group.approval.required;
}
if (this.task?.group?.sharedCompletion) {
this.sharedCompletion = this.task.group.sharedCompletion;
} else if (this.task.group) {
this.sharedCompletion = 'singleCompletion';
}
if (this.groupId) {
const members = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId,
includeAllPublicFields: true,
});
this.members = members;
this.membersNameAndId = [];
this.members.forEach(member => {
this.membersNameAndId.push({
id: member._id,
name: member.profile.name,
addlText: `@${member.auth.local.username}`,
});
this.memberNamesById[member._id] = member.profile.name;
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) {
this.assignedMembers = this.task.group.assignedUsers;
}
}
// @TODO: This whole component is mutating a prop
// and that causes issues. We need to not copy the prop similar to group modals
if (this.task) this.checklist = clone(this.task.checklist);
},
async handleOpen () {
this.syncTask();
},
cssClass (suffix) {
if (!this.task) {
return '';
@@ -1350,104 +1197,6 @@ export default {
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
calculateMonthlyRepeatDays (newRepeatsOn) {
if (!this.task) return;
const { task } = this;
@@ -1475,16 +1224,6 @@ export default {
if (!this.canSave) return;
if (this.newChecklistItem) this.addChecklistItem();
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
this.task.group.sharedCompletion = this.sharedCompletion;
this.task.managerNotes = this.managerNotes;
this.task.group.managerNotes = this.managerNotes;
}
if (this.task.type === 'reward' && this.task.value === '') {
this.task.value = 0;
}
@@ -1503,12 +1242,18 @@ export default {
tasks: [this.task],
});
Object.assign(this.task, response);
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
}));
Promise.all(promises);
this.task.group.assignedUsers = this.assignedMembers;
assignedUserIds: this.assignedMembers,
});
this.assignedMembers.forEach(memberId => {
if (!this.task.assignedUsersDetail) this.task.assignedUsersDetail = {};
this.task.assignedUsersDetail[memberId] = {
assignedDate: new Date(),
assigningUsername: this.user.auth.local.username,
completed: false,
};
});
this.$emit('taskCreated', this.task);
} else {
this.createTask(this.task);
@@ -1530,22 +1275,14 @@ export default {
this.$root.$emit('bv::hide::modal', 'task-modal');
},
onClose () {
if (this.task.group && this.task.group.managerNotes) this.managerNotes = null;
this.newChecklistItem = '';
this.$emit('cancel');
},
updateRequiresApproval (newValue) {
let truthy = true;
if (!newValue) truthy = false; // This return undefined instad of false
this.requiresApproval = truthy;
},
async toggleAssignment (memberId) {
if (this.purpose === 'create') {
return;
}
const assignedIndex = this.assignedMembers.indexOf(memberId);
if (assignedIndex === -1) {
await this.$store.dispatch('tasks:unassignTask', {
taskId: this.task._id,
@@ -1554,10 +1291,21 @@ export default {
} else {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
assignedUserIds: [memberId],
});
}
},
async clearAssignments () {
if (this.purpose === 'edit') {
for (const assignedMember of this.assignedMembers) {
await this.$store.dispatch('tasks:unassignTask', { // eslint-disable-line no-await-in-loop
taskId: this.task._id,
userId: assignedMember,
});
}
}
this.assignedMembers = [];
},
focusInput () {
this.$refs.inputToFocus.focus();
},
@@ -0,0 +1,358 @@
<template>
<b-modal
id="task-summary"
:hide-footer="true"
@hidden="$emit('cancel')"
>
<div
v-if="task"
slot="modal-header"
class="task-modal-header px-4 d-flex align-items-center"
:class="cssClass('bg')"
>
<h2
class="my-auto"
:class="cssClass('headings')"
>
{{ title }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
:class="cssClass('headings')"
aria-hidden="true"
tabindex="0"
@click="cancel()"
@keypress.enter="cancel()"
v-html="icons.close"
></div>
</div>
<div
v-if="task"
class="task-modal-content pt-3 px-4 pb-4"
>
<div class="summary-block">
<h3> {{ $t('title') }} </h3>
<p> {{ task.text }} </p>
</div>
<div
v-if="task.notes"
class="summary-block"
>
<h3> {{ $t('notes') }} </h3>
<p> {{ task.notes }} </p>
</div>
<div class="summary-block">
<h3> {{ $t('description') }} </h3>
<p v-html="summarySentence" ></p>
</div>
<div
v-if="task.checklist && task.checklist.length > 0"
class="summary-block"
>
<checklist
:items.sync="task.checklist"
:disableDrag="true"
:disableEdit="true"
/>
</div>
<div
class="summary-block"
v-if="assignedUsernames.length > 0"
>
<h3> {{ $t('assignedTo') }} </h3>
<div
class="d-flex flex-wrap"
>
<span
v-for="member in assignedUsernames"
:key="member"
class="assigned-member py-1 px-75 mb-1 mr-1"
>
@{{ member }}
</span>
</div>
</div>
</div>
<div
v-if="task && task.group && task.group.assignedUsersDetail
&& task.group.assignedUsersDetail[user._id]"
class="assignment-footer text-center py-2"
v-html="$t('assignedDateAndUser', {
username: task.group.assignedUsersDetail[user._id].assigningUsername,
date: formattedDate(task.group.assignedUsersDetail[user._id].assignedDate),
})"
>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#task-summary {
overflow-y: hidden;
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
.modal-dialog {
width: 448px;
margin-top: 50vh;
transform: translateY(-50%);
}
.modal-header {
padding: 0px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.assigned-member {
border: 1px solid $gray-400;
border-radius: 100px;
color: $gray-100;
font-size: 12px;
line-height: 1.33;
}
.assignment-footer {
color: $gray-100;
background-color: $gray-700;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.close-x {
height: 16px;
width: 16px;
position: relative;
opacity: 0.75;
cursor: pointer;
&:hover, &:focus {
opacity: 1;
}
}
.summary-block:not(:last-of-type) {
margin-bottom: 1rem;
}
.task-modal-content {
h3 {
margin-bottom: 0.25rem;
}
p {
margin-bottom: 0px;
}
h3, p {
color: $gray-50;
}
}
.task-modal-header {
color: $white;
height: 60px;
width: 100%;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
h2 {
color: $white;
}
}
</style>
<script>
import keys from 'lodash/keys';
import moment from 'moment';
import pickBy from 'lodash/pickBy';
import checklist from './modal-controls/checklist';
import { mapGetters, mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
export default {
components: {
checklist,
},
props: ['task'],
data () {
return {
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
icons: Object.freeze({
close: closeIcon,
}),
};
},
computed: {
...mapGetters({
getTaskClasses: 'tasks:getTaskClasses',
}),
...mapState({
user: 'user.data',
}),
assignedUsernames () {
if (!this.task.group || !this.task.group.assignedUsers
|| !this.task.group.assignedUsersDetail) return [];
const usernames = [];
for (const user of this.task.group.assignedUsers) {
usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername);
}
return usernames;
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
title () {
const type = this.$t(this.task.type);
return this.$t('taskSummary', { type });
},
},
methods: {
cancel () {
this.$root.$emit('bv::hide::modal', 'task-summary');
},
cssClass (suffix) {
if (!this.task) {
return '';
}
return this.getTaskClasses(this.task, `edit-modal-${suffix}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
},
};
</script>
+48 -33
View File
@@ -1,5 +1,8 @@
<template>
<div class="row user-tasks-page">
<div
class="row user-tasks-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<broken-task-modal />
<task-modal
ref="taskModal"
@@ -7,6 +10,11 @@
:purpose="creatingTask !== null ? 'create' : 'edit'"
@cancel="cancelTaskModal()"
/>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="col-12">
<div class="row tasks-navigation">
<div class="col-12 col-md-4 offset-md-4">
@@ -160,45 +168,43 @@
</div>
</div>
</div>
<div class="create-task-area d-flex">
<transition name="slide-tasks-btns">
<div class="create-task-area">
<div
id="create-task-btn"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
v-if="openCreateBtn"
class="d-flex"
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</div>
<div
v-if="openCreateBtn"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon"
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('addTask') }}
</b-tooltip>
</div>
</div>
<div class="row tasks-columns">
@@ -211,6 +217,7 @@
:search-text="searchTextThrottled"
:selected-tags="selectedTags"
@editTask="editTask"
@taskSummary="taskSummary"
@openBuyDialog="openBuyDialog($event)"
/>
</div>
@@ -345,7 +352,7 @@
}
.create-task-area {
top: -2.5rem;
top: 1px;
}
.drag {
@@ -381,6 +388,7 @@ import cloneDeep from 'lodash/cloneDeep';
import draggable from 'vuedraggable';
import TaskColumn from './column';
import TaskModal from './taskModal';
import TaskSummary from './taskSummary';
import spells from './spells';
import markdown from '@/directives/markdown';
@@ -401,6 +409,7 @@ export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
spells,
brokenTaskModal,
draggable,
@@ -524,13 +533,19 @@ export default {
};
this.newTag = null;
},
// Need Vue.nextTick() otherwise the first time the modal is not rendered
editTask (task) {
this.editingTask = cloneDeep(task);
// Necessary otherwise the first time the modal is not rendered
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
createTask (type) {
this.openCreateBtn = false;
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
@@ -84,6 +84,7 @@
import moment from 'moment';
import { mapState } from '@/libs/store';
import scoreTask from '@/mixins/scoreTask';
import sync from '@/mixins/sync';
import Task from './task';
import LoadingSpinner from '../ui/loadingSpinner';
@@ -92,7 +93,7 @@ export default {
Task,
LoadingSpinner,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
props: {
yesterDailies: {
type: Array,
@@ -180,6 +181,7 @@ export default {
this.isLoading = false;
this.$root.$emit('bv::hide::modal', 'yesterdaily');
if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync();
},
},
};
@@ -12,7 +12,7 @@
<template v-slot:button-content>
<slot
name="item"
:item="selected"
:item="selected || placeholder"
:button="true"
>
<!-- Fallback content -->
@@ -114,6 +114,9 @@ export default {
type: Boolean,
default: true,
},
placeholder: {
type: String,
},
},
data () {
return {
+5
View File
@@ -29,6 +29,11 @@ export default [
type: 'Staff',
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
},
{
name: 'heyeilatan',
type: 'Staff',
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
},
{
name: 'Alys',
type: 'Moderator',
@@ -17,8 +17,14 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
};
},
methods: {
hideSet (set) {
return moment(appearanceSets[set.key].availableUntil).isBefore(moment());
hideSet (setKey) {
if (appearanceSets[setKey].availableFrom) {
return !moment().isBetween(
appearanceSets[setKey].availableFrom,
appearanceSets[setKey].availableUntil,
);
}
return moment(appearanceSets[setKey].availableUntil).isBefore(moment());
},
mapKeysToFreeOption (key, type, subType) {
const userPreference = subType
@@ -43,8 +49,8 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
const locked = !userPurchased || !userPurchased[key];
let hide = false;
if (set && appearanceSets[set]) {
if (locked) hide = moment(appearanceSets[set].availableUntil).isBefore(moment());
if (set && appearanceSets[set] && locked) {
hide = this.hideSet(set);
}
option.gemLocked = locked;

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