Compare commits

...

54 Commits

Author SHA1 Message Date
Hafiz
867a5caa17 task stat selection UI updates 2025-12-02 09:05:46 -06:00
Hafiz
c1faac5385 show selected attribute on component itself 2025-12-01 13:52:44 -06:00
Hafiz
451da628a0 remove empty lines 2025-12-01 13:25:56 -06:00
Hafiz
13cea0fad4 Stat allocation by task dropdown 2025-12-01 13:17:46 -06:00
Hafiz
fa44b7e915 Remove bold text on hover 2025-11-26 13:10:40 -06:00
Hafiz
c3c04dc109 Update stat allocation dropdown styling 2025-11-26 11:31:19 -06:00
Hafiz
2553cc3435 Fix hover dropdown padding & stat allocation chip always showing 2025-11-21 13:17:04 -06:00
Hafiz
eebb78a766 stat allocation UI tweaks
refactor allocation mode selection with selectList component & other UI tweaks
2025-11-19 12:48:56 -06:00
Hafiz
4648b8e60a tweak 2025-11-18 12:18:24 -06:00
Hafiz
d37faef5f8 stat feedback UI updates 2025-11-12 11:38:42 -06:00
Hafiz
6a8a9f7842 lint fixes 2025-11-07 14:09:41 -06:00
Hafiz
726c2c18b8 Assigned Stat 2025-11-07 14:07:43 -06:00
Hafiz
3a4f6363fe Stat allocation updates 2025-11-07 14:01:11 -06:00
Hafiz
9093dc4e20 Auto allocate UI fixes
- remove info icon in each dropdown option, and replace info below each option
- disable auto allocate dropdown instead of hiding it
- removed radio buttons drop auto allocate options dropdown
- Points available & auto allocate text share same baseline now
2025-10-27 13:22:02 -05:00
Hafiz
21fd83695a lint fixes 2025-10-21 14:59:16 -05:00
Hafiz
2c44c37a96 Fix auto allocation toggle alignment 2025-10-21 14:47:17 -05:00
Hafiz
f0adb85660 Remove duplicate string value, convert line endings 2025-10-21 14:43:03 -05:00
Hafiz
c80b94178b Merge branch 'fiz/stats-allocation' into qa/monkey 2025-10-21 14:29:38 -05:00
Hafiz
c85018a721 Live stat allocation & auto allocation implemented
- Update stat allocation UI/UX cont.
- Update stats live (versus pressing save)
- Dynamically show the distribute dropdown based on if auto allocate switch is enabled
2025-10-21 14:29:15 -05:00
Hafiz
26a65d778b Update styling of stat boxes & add stat allocation info 2025-10-21 13:13:36 -05:00
Hafiz
5bc4d7e99d Update order of views on Stats tab 2025-10-21 12:51:42 -05:00
Hafiz
20437c4188 Fixed cut off modal issues
- Fixed limited time banner being cut off on smaller devices
- Fixed Pin on purchase quest modal being cut off
2025-10-16 13:04:02 -05:00
Hafiz
e8c04e1109 Merge branch 'develop' into qa/monkey 2025-10-16 12:49:04 -05:00
Kalista Payne
e2d2a05315 docs(migration): remove outdated comment 2025-10-14 11:17:51 -05:00
Kalista Payne
be041f734d fix(lint): unsupported operator 2025-10-14 11:17:11 -05:00
Muhammad Ubaid Nawaz
c430d2279c fix(migrations->users->take-this.js): Fixed processUsers function by updating query._id to match only the user's _id instead of the entire user object (#15511) 2025-10-01 13:20:43 -05:00
Kalista Payne
ef592cf35f 5.41.4 2025-09-30 17:06:15 -05:00
Kalista Payne
f24cd10a79 chore(subproj): update habitica-images 2025-09-30 17:06:09 -05:00
Weblate
2cd4e45016 Translated using Weblate (Croatian)
Currently translated at 100.0% (8 of 8 strings)

Merge branch 'origin/develop' into Weblate.

Translated using Weblate (Croatian)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Croatian)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 94.6% (3258 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 93.1% (54 of 58 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 83.7% (227 of 271 strings)

Translated using Weblate (Russian)

Currently translated at 83.3% (226 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 92.7% (382 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 88.5% (365 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 82.7% (341 of 412 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 87.9% (80 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 78.0% (71 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 60.4% (55 of 91 strings)

Translated using Weblate (Croatian)

Currently translated at 43.9% (40 of 91 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.9% (3163 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.8% (3160 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Polish)

Currently translated at 34.2% (84 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.1% (717 of 862 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.0% (267 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 86.4% (96 of 111 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.4% (245 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.6% (266 of 284 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (860 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (258 of 275 strings)

Translated using Weblate (Polish)

Currently translated at 89.0% (253 of 284 strings)

Translated using Weblate (Polish)

Currently translated at 51.7% (1780 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 94.3% (3248 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 85.9% (233 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (859 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 94.3% (3245 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 84.5% (229 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.7% (255 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 94.1% (3240 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 95.3% (822 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Russian)

Currently translated at 76.3% (187 of 245 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 91.6% (394 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 94.0% (3237 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 96.6% (892 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Polish)

Currently translated at 91.5% (173 of 189 strings)

Translated using Weblate (Polish)

Currently translated at 98.5% (271 of 275 strings)

Translated using Weblate (Polish)

Currently translated at 97.6% (420 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Polish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Polish)

Currently translated at 95.8% (412 of 430 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (French)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 93.5% (3219 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.0% (2409 of 3441 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 36.3% (89 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.4% (192 of 193 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 70.9% (195 of 275 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (189 of 189 strings)

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

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* add blocker to block emails from registration

* lint fixes

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

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

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

* Update ToS error message

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

* lint fix

* Responsive Layout for Equipment Containers

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

* remove redundant disabled styles in task modals

The .disabled class conflicting with existing disabled state implementations

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

This reverts commit 4f28bfaad4, reversing
changes made to 477dd6328a.

* fix(blockers): duplicated code from rebase

* fix(admin): revert accidental change from rebase

* move !error.response to correct level

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

* chore(github): split responsiveness to #15514

* Group string updates & removals

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

* Remove some Group strings

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

---------

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

* Fetch members in batches until members are loaded

* Fix challenge winner search to load all participants

Separated loading flags to prevent conflicts between modals

* Rename end challenge members flag to be more clear

* await load members

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

Translated using Weblate (Russian)

Currently translated at 98.3% (423 of 430 strings)

Translated using Weblate (Croatian)

Currently translated at 81.1% (220 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.3% (3211 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3206 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3205 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 97.5% (239 of 245 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.1% (238 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 93.1% (3204 of 3441 strings)

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

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* add blocker to block emails from registration

* lint fixes

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

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

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

* Update ToS error message

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

* lint fix

* Responsive Layout for Equipment Containers

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

* remove redundant disabled styles in task modals

The .disabled class conflicting with existing disabled state implementations

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

This reverts commit 4f28bfaad4, reversing
changes made to 477dd6328a.

* fix(blockers): duplicated code from rebase

* fix(admin): revert accidental change from rebase

* move !error.response to correct level

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

* chore(github): split responsiveness to #15514

---------

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (German)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (French)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (French)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (French)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (840 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 12.2% (30 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 49.6% (1707 of 3441 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 86.3% (797 of 923 strings)

Translated using Weblate (Croatian)

Currently translated at 78.2% (212 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 83.8% (238 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 95.3% (184 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 65.3% (177 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 85.7% (162 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 11.4% (28 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 59.7% (162 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 71.9% (82 of 114 strings)

Translated using Weblate (Croatian)

Currently translated at 84.6% (160 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 10.2% (25 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 47.2% (128 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 83.4% (237 of 284 strings)

Translated using Weblate (Croatian)

Currently translated at 71.4% (135 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 6.5% (16 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (Croatian)

Currently translated at 79.9% (195 of 244 strings)

Translated using Weblate (Croatian)

Currently translated at 4.0% (10 of 245 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 70.8% (134 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 70.8% (134 of 189 strings)

Translated using Weblate (Croatian)

Currently translated at 93.7% (181 of 193 strings)

Translated using Weblate (Croatian)

Currently translated at 44.6% (121 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 50.1% (137 of 273 strings)

Translated using Weblate (Japanese)

Currently translated at 93.0% (3201 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 92.9% (3200 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 95.3% (822 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 96.7% (237 of 245 strings)

Translated using Weblate (Korean)

Currently translated at 79.3% (732 of 923 strings)

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

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

* Fix undefined userId

* Server now matches usernames case insensitively like client

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

* lint fixes

* g1g1 width auto sizing w/padding

* Challenge participants spacing & text sizing fix

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

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

* Update End Challenge modal

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

* lint error fixes

* end challenge modal fixes

* lint fix

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

* fix lint

* Delete icon color to match text on close challenge modal

use color field to set delete icon color

* Highlight username on close challenge modal color updates

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

* Fix strings

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

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

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

* Update prize card to match participants card on challenges

* End Challenge modal UI tweaks

* Prevent false mention highlights

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

* lint fixes

* Remove mention highlight

* Mention highlighting to only highlight w/username mentions

* Update G1G1 Notification

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

* lint fix

* Add updated G1G1 notification SVGs

* Don't highlight display name w/mention

* g1g1 UI updates

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

* Fix g1g1 close icon hover state

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

* g1g1 close hover state fix

* End challenge UI updates

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

* remove trailing space

* Fix exit hover state on g1g1

* fix g1g1 close icon (directly render close icon)

* new line

* Update z-index of g1g1 close button

* add display name support for mention highlighting

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

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

(Also revert the renderWithMentions change)

* Fix mention display name test (& fix lint)

* Revert display name mention, strictly only username

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

* Improved case-insensitive username matching

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

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

* fix lint errors

* end challenge modal UI updates

* Don't change gem color on update

* disabled state button match button.scss syling

* remove padding from g1g1 close

* Directly use button.scss on end challenge modal

* Update disabled state for button.scss

* explicitly set close challenge modal button disabled/enabled state

* fix trailing space

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

* Update award winner button min-height & padding

* button.scss button disabled styling updates

* Remove redundant disabled override on award winner button

* lint

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

Remove unneeded gifts_end.svg

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

* Removed the 0.5em padding w/p-2

* added v-once to the refund text element

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

Translated using Weblate (Japanese)

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Polish)

Currently translated at 51.6% (1776 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (192 of 193 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 96.3% (236 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 36.3% (89 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 35.1% (86 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 84.0% (776 of 923 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 94.8% (818 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 92.6% (3189 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 94.6% (232 of 245 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3440 of 3441 strings)

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

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

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

* fix(gpc): style tweaks

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

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

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

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

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

Translated using Weblate (Japanese)

Currently translated at 99.6% (270 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 93.4% (229 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.1% (906 of 923 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 91.7% (3157 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3117 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3117 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Italian)

Currently translated at 89.6% (243 of 271 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3439 of 3441 strings)

Translated using Weblate (German)

Currently translated at 100.0% (273 of 273 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (923 of 923 strings)

Translated using Weblate (Japanese)

Currently translated at 92.5% (3186 of 3441 strings)

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

* remove unused imports

* add delete button to adminpanel

* June 2025 content build (#15437)

* chore: June 2025 content build

* chore: typo fixing

* chore: corrections to summer 2025 mage armor, spritesheet

* fix(css): rebuild spritesmith-main

---------

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

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

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

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (French)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (279 of 280 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.8% (907 of 908 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (219 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 28.1% (69 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.5% (402 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 91.5% (377 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 85.2% (774 of 908 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 63.4% (106 of 167 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 2.0% (5 of 245 strings)

Translated using Weblate (French)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Russian)

Currently translated at 64.4% (158 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.0% (837 of 862 strings)

Translated using Weblate (German)

Currently translated at 97.9% (844 of 862 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.3% (401 of 412 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (393 of 412 strings)

Translated using Weblate (Slovak)

Currently translated at 45.6% (413 of 905 strings)

Translated using Weblate (Slovak)

Currently translated at 50.8% (85 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3324 of 3325 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (429 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.6% (3114 of 3325 strings)

Translated using Weblate (Portuguese)

Currently translated at 53.9% (1793 of 3325 strings)

Translated using Weblate (Dutch)

Currently translated at 78.1% (2600 of 3325 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (242 of 243 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.6% (398 of 412 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 87.3% (2903 of 3325 strings)

Translated using Weblate (Italian)

Currently translated at 17.1% (42 of 245 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (408 of 412 strings)

Translated using Weblate (Italian)

Currently translated at 92.7% (102 of 110 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.0% (3292 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (905 of 905 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.1% (3262 of 3325 strings)

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

* 5.36.4

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* optimize query when listing challenge tasks

* Optimize query for checking if user is party leader

* correct worker call

* remove unused priority

* fix tests

* don’t use body with delete

* add detailed information about sub payment for google and apple

* Support paypal details for subscription in admin panel

* stripe payment details

* fix imports

* fix tests

* fix deleting account

* begin building group admin panel

* fix convertig sub to group plan

* improve sub status display

* fix lint

* fix long line

* fix sub state display

* lint fix

* fix

* delete amplitude data by default

* improve searching for email in admin panel

* correctly call method

* move delete button in admin panel

* fix(lint): whitespace

* fix(style): indent

* fix(typo): humand

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Andrea <goffopaguro@gmail.com>
Co-authored-by: Artem StolyROV <stolyarov11303@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kaya <david@kaya.sk>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Irina  Shcherbinina <cat3dcat007@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Tom <tompsognathus@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: V Aar <v.vanderaar@gmail.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Волкозмей <klippiky@gmail.com>
Co-authored-by: Данила Мальцев <maltsev-danila@inbox.ru>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-09-09 16:41:03 -05:00
Kalista Payne
7ecb83dc7e 5.40.2 2025-09-08 15:40:45 -05:00
Weblate
e8ffe2286c Translated using Weblate (Japanese)
Currently translated at 92.5% (3183 of 3441 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (283 of 284 strings)

Translated using Weblate (German)

Currently translated at 99.2% (3415 of 3441 strings)

Translated using Weblate (German)

Currently translated at 98.7% (3397 of 3441 strings)

Translated using Weblate (Ukrainian)

Currently translated at 56.6% (1948 of 3441 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.1% (760 of 862 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (284 of 284 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.8% (278 of 284 strings)

Translated using Weblate (Japanese)

Currently translated at 92.2% (3174 of 3441 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3441 of 3441 strings)

Translated using Weblate (German)

Currently translated at 98.2% (3382 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 85.7% (210 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.3% (825 of 923 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 87.8% (811 of 923 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 87.1% (804 of 923 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 30.2% (74 of 245 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 29.7% (73 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 83.0% (157 of 189 strings)

Translated using Weblate (Japanese)

Currently translated at 92.1% (3171 of 3441 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 82.6% (224 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (108 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 82.0% (155 of 189 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.3% (917 of 923 strings)

Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de>
Co-authored-by: Luizo <t.czj2019@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Mateus Scheper <mateus_scheper@hotmail.com>
Co-authored-by: Sara Olson <sara@habitica.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Музика Анастасія <ukrainianbimba25@gmail.com>
Co-authored-by: インコ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Limited
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2025-09-08 22:12:28 +02:00
199 changed files with 5806 additions and 2451 deletions

View File

@@ -73,7 +73,7 @@ export default async function processUsers () {
break;
} else {
query._id = {
$gt: users[users.length - 1],
$gt: users[users.length - 1]._id,
};
}

103
package-lock.json generated
View File

@@ -1,18 +1,17 @@
{
"name": "habitica",
"version": "5.40.1",
"version": "5.41.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.40.1",
"version": "5.41.4",
"hasInstallScript": true,
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@google-analytics/data": "^4.12.1",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
@@ -1975,17 +1974,6 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/@google-analytics/data": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/@google-analytics/data/-/data-4.12.1.tgz",
"integrity": "sha512-LzyrkVrnVUTYTmdmHayOZoroc+YA9GHEUrkSSuiXSmMSNbesuWy/MoTXugC1V7+8PCGqb2eQ1UtVVv/2BCAQYA==",
"dependencies": {
"google-gax": "^4.0.3"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google-cloud/common": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-4.0.3.tgz",
@@ -2329,6 +2317,7 @@
"version": "1.10.8",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.8.tgz",
"integrity": "sha512-vYVqYzHicDqyKB+NQhAc54I1QWCBLCrYG6unqOIcBTHx+7x8C9lcoLj3KVJXs2VB4lUbpWY+Kk9NipcbXYWmvg==",
"optional": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -2341,6 +2330,7 @@
"version": "0.7.13",
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
"integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
"optional": true,
"dependencies": {
"lodash.camelcase": "^4.3.0",
"long": "^5.0.0",
@@ -2358,6 +2348,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -2372,6 +2363,7 @@
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"optional": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
@@ -2385,6 +2377,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"optional": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -2395,12 +2388,14 @@
"node_modules/@grpc/proto-loader/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"optional": true
},
"node_modules/@grpc/proto-loader/node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"optional": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -2409,6 +2404,7 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -2425,6 +2421,7 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"optional": true,
"engines": {
"node": ">=10"
}
@@ -2433,6 +2430,7 @@
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"optional": true,
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
@@ -2450,6 +2448,7 @@
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"optional": true,
"engines": {
"node": ">=12"
}
@@ -2620,6 +2619,7 @@
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
@@ -2932,27 +2932,32 @@
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"optional": true
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"optional": true
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"optional": true
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"optional": true
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"optional": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
@@ -2961,27 +2966,32 @@
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"optional": true
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"optional": true
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"optional": true
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"optional": true
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"optional": true
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
@@ -3116,7 +3126,8 @@
"node_modules/@types/caseless": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg=="
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
"optional": true
},
"node_modules/@types/connect": {
"version": "3.4.38",
@@ -3219,7 +3230,8 @@
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"optional": true
},
"node_modules/@types/mime": {
"version": "1.3.5",
@@ -3269,6 +3281,7 @@
"version": "2.48.12",
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
"integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
"optional": true,
"dependencies": {
"@types/caseless": "*",
"@types/node": "*",
@@ -3280,6 +3293,7 @@
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",
"integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",
"optional": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
@@ -3319,7 +3333,8 @@
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
"optional": true
},
"node_modules/@types/triple-beam": {
"version": "1.3.5",
@@ -3529,6 +3544,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"optional": true,
"dependencies": {
"event-target-shim": "^5.0.0"
},
@@ -6070,9 +6086,10 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -9346,6 +9363,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"optional": true,
"engines": {
"node": ">=6"
}
@@ -11431,9 +11449,10 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
@@ -11597,6 +11616,7 @@
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.3.tgz",
"integrity": "sha512-f4F2Y9X4+mqsrJuLZsuTljYuQpcBnQsCt9ScvZpdM8jGjqrcxyJi5JUiqtq0jtpdHVPzyit0N7f5t07e+kH5EA==",
"optional": true,
"dependencies": {
"@grpc/grpc-js": "~1.10.3",
"@grpc/proto-loader": "^0.7.0",
@@ -11619,6 +11639,7 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"optional": true,
"dependencies": {
"debug": "^4.3.4"
},
@@ -11630,6 +11651,7 @@
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz",
"integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==",
"optional": true,
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
@@ -11645,6 +11667,7 @@
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
"integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
"optional": true,
"dependencies": {
"gaxios": "^6.0.0",
"json-bigint": "^1.0.0"
@@ -11657,6 +11680,7 @@
"version": "9.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz",
"integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==",
"optional": true,
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
@@ -11673,6 +11697,7 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"optional": true,
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
@@ -11685,6 +11710,7 @@
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
"integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
"optional": true,
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
@@ -11698,6 +11724,7 @@
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz",
"integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
@@ -11720,6 +11747,7 @@
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
"optional": true,
"dependencies": {
"@types/request": "^2.48.8",
"extend": "^3.0.2",
@@ -11733,6 +11761,7 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
"optional": true,
"dependencies": {
"http-proxy-agent": "^5.0.0",
"https-proxy-agent": "^5.0.0",
@@ -11748,6 +11777,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"optional": true,
"dependencies": {
"debug": "4"
},
@@ -11759,6 +11789,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"optional": true,
"dependencies": {
"agent-base": "6",
"debug": "4"
@@ -14237,7 +14268,8 @@
"node_modules/lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"optional": true
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
@@ -16660,6 +16692,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"optional": true,
"engines": {
"node": ">= 6"
}
@@ -17882,6 +17915,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.1.tgz",
"integrity": "sha512-8awBvjO+FwkMd6gNoGFZyqkHZXCFd54CIYTb6De7dPaufGJ2XNW+QUNqbMr8MaAocMdb+KpsD4rxEOaTBDCffA==",
"optional": true,
"dependencies": {
"protobufjs": "^7.2.5"
},
@@ -17894,6 +17928,7 @@
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz",
"integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",

View File

@@ -1,13 +1,12 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.40.1",
"version": "5.41.4",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@google-analytics/data": "^4.12.1",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",

View File

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

View File

@@ -47,6 +47,12 @@ describe('highlightMentions', () => {
expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)');
});
it('highlights users with case-insensitive matching', async () => {
const text = '@USER: message @User2 @USER3';
const result = await highlightMentions(text);
expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)');
});
it('doesn\'t highlight nonexisting users', async () => {
const text = '@nouser message';
const result = await highlightMentions(text);

View File

@@ -12,11 +12,33 @@ const { i18n } = common;
describe('Apple Payments', () => {
const subKey = 'basic_3mo';
let iapSetupStub;
let iapValidateStub;
let iapIsValidatedStub;
let iapIsCanceledStub;
let iapIsExpiredStub;
let paymentBuySkuStub;
let iapGetPurchaseDataStub;
let validateGiftMessageStub;
let paymentsCreateSubscritionStub;
beforeEach(() => {
iapSetupStub = sinon.stub(iap, 'setup').resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
});
describe('verifyPurchase', () => {
let sku; let user; let token; let receipt; let
headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
iapGetPurchaseDataStub; let validateGiftMessageStub;
beforeEach(() => {
token = 'testToken';
@@ -25,13 +47,9 @@ describe('Apple Payments', () => {
receipt = `{"token": "${token}", "productId": "${sku}"}`;
headers = {};
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isCanceled').returns(false);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
productId: 'com.habitrpg.ios.Habitica.21gems',
@@ -42,12 +60,6 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
@@ -209,9 +221,6 @@ describe('Apple Payments', () => {
describe('subscribe', () => {
let sub; let sku; let user; let token; let receipt; let headers; let
nextPaymentProcessing;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub;
let paymentsCreateSubscritionStub; let
iapGetPurchaseDataStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
@@ -223,12 +232,10 @@ describe('Apple Payments', () => {
nextPaymentProcessing = moment.utc().add({ days: 2 });
user = new User();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
@@ -250,10 +257,6 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
@@ -270,6 +273,29 @@ describe('Apple Payments', () => {
});
});
it('should throw an error if no active subscription is found', async () => {
iap.isCanceled.restore();
iapIsCanceledStub = sinon.stub(iap, 'isCanceled')
.returns(true);
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: -2 }).toDate(),
purchaseDate: new Date(),
productId: 'subscription1month',
transactionId: token,
originalTransactionId: token,
}]);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
const subOptions = [
{
sku: 'subscription1month',
@@ -574,8 +600,7 @@ describe('Apple Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let headers; let customerId; let
expirationDate;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
let paymentCancelSubscriptionSpy;
beforeEach(async () => {
token = 'test-token';
@@ -584,8 +609,7 @@ describe('Apple Payments', () => {
customerId = 'test-customerId';
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub.restore();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({
expirationDate,
@@ -593,8 +617,8 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(true);
iapIsCanceledStub = sinon.stub(iap, 'isCanceled').returns(false);
iapIsExpiredStub = sinon.stub(iap, 'isExpired').returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -606,13 +630,7 @@ describe('Apple Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
paymentCancelSubscriptionSpy.restore();
});
it('should throw an error if we are missing a subscription', async () => {
@@ -695,6 +713,8 @@ describe('Apple Payments', () => {
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate,
});
expect(iapIsCanceledStub).to.be.calledOnce;
expect(iapIsExpiredStub).to.be.calledOnce;
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;

View File

@@ -11,12 +11,36 @@ const { i18n } = common;
describe('Google Payments', () => {
const subKey = 'basic_3mo';
let iapSetupStub;
let iapValidateStub;
let iapIsValidatedStub;
let paymentBuySkuStub;
let validateGiftMessageStub;
beforeEach(() => {
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(false);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isCanceled.restore();
iap.isExpired.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
describe('verifyPurchase', () => {
let sku; let user; let token; let receipt; let signature; let
headers;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentBuySkuStub; let validateGiftMessageStub;
beforeEach(() => {
sku = 'com.habitrpg.android.habitica.iap.21gems';
@@ -25,21 +49,7 @@ describe('Google Payments', () => {
signature = '';
headers = {};
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
});
it('should throw an error if receipt is invalid', async () => {
@@ -160,8 +170,7 @@ describe('Google Payments', () => {
describe('subscribe', () => {
let sub; let sku; let user; let token; let receipt; let signature; let headers; let
nextPaymentProcessing;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
paymentsCreateSubscritionStub;
let paymentsCreateSubscritionStub;
beforeEach(() => {
sub = common.content.subscriptionBlocks[subKey];
@@ -173,19 +182,12 @@ describe('Google Payments', () => {
signature = '';
nextPaymentProcessing = moment.utc().add({ days: 2 });
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
paymentsCreateSubscritionStub = sinon.stub(payments, 'createSubscription').resolves({});
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
payments.createSubscription.restore();
});
@@ -243,7 +245,7 @@ describe('Google Payments', () => {
describe('cancelSubscribe ', () => {
let user; let token; let receipt; let signature; let headers; let customerId; let
expirationDate;
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let iapGetPurchaseDataStub; let
let iapGetPurchaseDataStub; let
paymentCancelSubscriptionSpy;
beforeEach(async () => {
@@ -253,17 +255,12 @@ describe('Google Payments', () => {
signature = '';
customerId = 'test-customerId';
expirationDate = moment.utc();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({
expirationDate,
});
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate(), autoRenewing: false }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
user = new User();
user.profile.name = 'sender';
@@ -276,9 +273,6 @@ describe('Google Payments', () => {
});
afterEach(() => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
@@ -308,6 +302,8 @@ describe('Google Payments', () => {
});
it('should cancel a user subscription', async () => {
iap.isCanceled.restore();
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
await googlePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
@@ -332,11 +328,20 @@ describe('Google Payments', () => {
});
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
iap.isCanceled.restore();
iap.isCanceled = sinon.stub(iap, 'isCanceled').returns(true);
const laterDate = moment.utc().add(7, 'days');
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate, autoRenewing: false },
{ expirationDate: laterDate, autoRenewing: false },
.returns([{
startTimeMillis: expirationDate.valueOf(),
expirationDate,
autoRenewing: false,
}, {
startTimeMillis: laterDate.valueOf(),
expirationDate: laterDate,
autoRenewing: false,
},
]);
await googlePayments.cancelSubscribe(user, headers);
@@ -365,7 +370,12 @@ describe('Google Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ autoRenewing: true }]);
await googlePayments.cancelSubscribe(user, headers);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -388,8 +398,12 @@ describe('Google Payments', () => {
.returns([{ expirationDate, autoRenewing: false },
{ autoRenewing: true },
{ expirationDate, autoRenewing: false }]);
await googlePayments.cancelSubscribe(user, headers);
await expect(googlePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: googlePayments.constants.RESPONSE_STILL_VALID,
});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {

View File

@@ -238,6 +238,18 @@ describe('POST /chat', () => {
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with case-insensitive mentions', async () => {
const originalUsername = member.auth.local.username;
const uppercaseUsername = originalUsername.toUpperCase();
const messageWithMentions = `hi @${uppercaseUsername}`;
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions });
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`);
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with a max length of 3000 chars', async () => {
const veryLongMessage = `
123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789.

View File

@@ -44,7 +44,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id, username: user.auth.local.username }),
});
});

View File

@@ -23,7 +23,6 @@
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
"eslint-plugin-vue": "7.20.0",
"ga-gtag": "^1.2.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"intro.js": "^7.2.0",
@@ -38,7 +37,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
@@ -5123,6 +5122,20 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -5161,11 +5174,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ga-gtag": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ga-gtag/-/ga-gtag-1.2.0.tgz",
"integrity": "sha512-j9gxutMdpGMdwaX1SzOG31Ddm+IGFjeNf+N3Z5g+BBpS8FSXOALlrM+ORIGc/QKszGJEDlw+6PfIsJZICsqsGQ=="
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -7126,6 +7134,21 @@
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@@ -8528,9 +8551,10 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

View File

@@ -27,7 +27,6 @@
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
"eslint-plugin-vue": "7.20.0",
"ga-gtag": "^1.2.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"intro.js": "^7.2.0",
@@ -42,7 +41,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",

View File

@@ -203,6 +203,9 @@ export default {
return response;
}, error => { // Set up Error interceptors
if (!error.response) {
return Promise.reject(error);
}
if (error.response.status >= 400) {
const isBanned = this.checkForBannedUser(error);
if (isBanned === true) return null; // eslint-disable-line consistent-return

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

After

Width:  |  Height:  |  Size: 448 B

View File

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

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -30,12 +30,23 @@
cursor: default;
color: $gray-200;
opacity: 1;
box-shadow: none;
background-color: $gray-700;
background-color: transparent;
border: 2px solid transparent;
box-shadow:
0 1px 3px 0 rgba($black, 0.12),
0 1px 2px 0 rgba($black, 0.24);
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
padding: 4px 12px;
min-height: 32px;
max-height: 32px;
gap: 8px;
border-radius: 4px;
.svg {
color: $gray-300;
color: $gray-200;
}
}

View File

@@ -123,6 +123,10 @@ h4 {
background-color: $purple-300 !important;
}
.bg-yellow-50 {
background-color: $yellow-50 !important;
}
.bg-white {
background-color: $white !important;
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g fill="#FFFFFF" fill-rule="nonzero">
<polygon points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 504 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

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

View File

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

View File

@@ -5,6 +5,12 @@
class="row"
>
<div class="form col-12">
<button
class="btn btn-danger mt-3 float-right"
@click="confirmDeleteHero"
>
Begin Member deletion
</button>
<basic-details
:user-id="hero._id"
:auth="hero.auth"
@@ -96,6 +102,53 @@
:reset-counter="resetCounter"
@clear-data="clearData"
/>
<b-modal
id="delete-member-modal"
title="Delete Member"
ok-title="Delete"
ok-variant="danger"
cancel-title="Cancel"
@ok="deleteHero"
>
<b-modal-body>
<p>
Are you sure you want to delete this member?
</p>
<p class="errorMessage">
Please note: This action cannot be undone!
</p>
<div class="ml-4">
<div class="form-check">
<input
id="deleteAccountCheck"
v-model="deleteHabiticaAccount"
class="form-check-input"
type="checkbox"
>
<label
class="form-check-label"
for="deleteAccountCheck"
>
Delete Habitica account
</label>
</div>
<div class="form-check">
<input
id="deleteAmplitudeCheck"
v-model="deleteAmplitudeData"
class="form-check-input"
type="checkbox"
>
<label
class="form-check-label"
for="deleteAmplitudeCheck"
>
Delete Amplitude data
</label>
</div>
</div>
</b-modal-body>
</b-modal>
</div>
</div>
</div>
@@ -184,6 +237,8 @@ export default {
hasParty: false,
partyNotExistError: false,
adminHasPrivForParty: true,
deleteHabiticaAccount: true,
deleteAmplitudeData: true,
};
},
watch: {
@@ -249,6 +304,25 @@ export default {
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
confirmDeleteHero () {
if (this.hero._id === this.user._id) {
window.alert('You cannot delete your own account.');
return;
}
this.$root.$emit('bv::show::modal', 'delete-member-modal');
},
deleteHero () {
this.$store.dispatch('hall:deleteHero', {
uuid: this.hero._id,
deleteHabiticaAccount: this.deleteHabiticaAccount,
deleteAmplitudeData: this.deleteAmplitudeData,
}).then(() => {
this.$root.$emit('bv::hide::modal', 'delete-member-modal');
this.$router.push({ name: 'adminPanel' });
}).catch(err => {
window.alert(err);
});
},
hasUnsavedChanges (...comparisons) {
for (const index in comparisons) {
if (index && comparisons[index]) {

View File

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

View File

@@ -15,6 +15,25 @@
:class="{ 'open': expand }"
>
Subscription, Monthly Perks
<span
v-if="isSubscribed() && !isCancelled()"
class="text-success float-right ml-3"
>
Active
</span>
<span
v-else-if="isSubscribed() && isCancelled()"
class="text-success float-right ml-3"
>
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
</span>
<span
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
class="text-warning float-right ml-3"
>
Inactive
</span>
<b
v-if="hasUnsavedChanges && !expand"
class="text-warning float-right"
@@ -46,7 +65,7 @@
class="form-control"
type="text"
>
<option value="groupPlan">
<option value="Group Plan">
Group Plan
</option>
<option value="Stripe">
@@ -154,7 +173,11 @@
>
<div class="card-body">
<h6 class="card-title">
{{ group.name }}
<router-link
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
>
{{ group.name }}
</router-link>
<small class="float-right">{{ group._id }}</small>
</h6>
<p class="card-text">
@@ -245,8 +268,7 @@
</div>
</div>
<small
v-if="!hero.purchased.plan.dateTerminated
&& hero.purchased.plan.planId"
v-if="isSubscribed() && !isCancelled()"
class="text-success"
>
The subscription does not have a termination date and is active.
@@ -419,6 +441,79 @@
>
</div>
</div>
<div class="form-group row">
<h2>Payment Details</h2>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9 mb-3">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="getSubscriptionPaymentDetails"
>
Get Subscription Payment Details
</button>
</div>
</div>
<div
v-if="paymentDetails"
>
<div
v-for="(value, key) in paymentDetails"
:key="key"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
{{ getHumanReadablePaymentDetails(key).label }}:
<span
:id="`${key}_tooltip`"
v-b-tooltip.hover.right="getHumanReadablePaymentDetails(key).help"
class="info-icon"
>?</span>
</label>
<strong class="col-sm-9 col-form-label">
<span v-if="value === true">Yes</span>
<span v-else-if="value === false">No</span>
<span
v-else-if="value instanceof String && isDate(value)"
v-b-tooltip.hover="value"
>
{{ formatDate(value) }}
</span>
<span v-else-if="value === null">---</span>
<span v-else>{{ value }}</span>
</strong>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<a
v-if="hero.purchased.plan.paymentMethod === 'Google'"
class="btn btn-primary btn-sm"
target="_blank"
:href="playOrdersUrl"
>
Play Console
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
>
PayPal Dashboard
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
>
Stripe Dashboard
</a>
</div>
</div>
</div>
</div>
<div
v-if="expand"
@@ -474,17 +569,36 @@
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
.form-group {
margin-bottom: 0.4rem;
}
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
}
}
.info-icon {
font-size: 0.8rem;
color: $purple-400;
cursor: pointer;
margin-left: 0.2rem;
background-color: $gray-500;
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
}
.info-icon:hover {
background-color: $purple-400;
color: white;
}
}
</style>
<script>
@@ -495,6 +609,55 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner';
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
const humanReadablePaymentDetails = {
customerId: {
label: 'Customer ID',
help: 'The unique identifier for the customer in the payment system.',
},
purchaseDate: {
label: 'Purchase Date',
help: 'The date when the subscription was purchased or renewed.',
},
originalPurchaseDate: {
label: 'Original Purchase Date',
help: 'The date when the subscription was first purchased.',
},
productId: {
label: 'Product ID',
help: 'The identifier for the product associated with the subscription.',
},
transactionId: {
label: 'Transaction ID',
help: 'The unique identifier for the last transaction in the payment system.',
},
isCanceled: {
label: 'Is Canceled',
help: 'Indicates whether the subscription has been canceled by the user or the system.',
},
isExpired: {
label: 'Is Expired',
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
},
expirationDate: {
label: 'Termination Date',
help: 'The date when the subscription will expire or has expired.',
},
nextPaymentDate: {
label: 'Next Payment Date',
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
},
lastPaymentDate: {
label: 'Last Payment Date',
help: 'The date when the lastpayment was made for the subscription.',
},
failedPayments: {
label: 'Failed Payments',
help: 'Number of times the payment failed for this subscription.',
},
};
export default {
components: {
LoadingSpinner,
@@ -520,6 +683,7 @@ export default {
isConvertingToGroupPlan: false,
groupPlanID: '',
subscriptionBlocks,
paymentDetails: null,
};
},
computed: {
@@ -553,6 +717,9 @@ export default {
}
return terminationDate;
},
playOrdersUrl () {
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
},
},
methods: {
dateFormat (date) {
@@ -583,6 +750,20 @@ export default {
this.isConvertingToGroupPlan = true;
this.hero.purchased.plan.owner = '';
},
getSubscriptionPaymentDetails () {
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
.then(details => {
if (details) {
this.paymentDetails = details;
} else {
alert('No payment details found.');
}
})
.catch(error => {
console.error('Error fetching subscription payment details:', error);
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
});
},
saveClicked (e) {
e.preventDefault();
if (this.isConvertingToGroupPlan) {
@@ -601,6 +782,31 @@ export default {
this.$emit('changeUserIdentifier', id);
}
},
getHumanReadablePaymentDetails (key) {
return humanReadablePaymentDetails[key] || { label: key, help: '' };
},
isDate (date) {
return moment(date).isValid();
},
formatDate (date) {
return date ? moment(date).format('MM/DD/YYYY') : '---';
},
isSubscribed () {
console.log(this.hero.purchased.plan.customerId, this.hero.purchased.plan.dateTerminated);
return this.hero.purchased.plan
&& this.hero.purchased.plan.customerId
&& this.hero.purchased.plan.planId
&& this.hero.purchased.plan.paymentMethod
&& (
!this.hero.purchased.plan.dateTerminated
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
);
},
isCancelled () {
return this.hero.purchased.plan
&& this.hero.purchased.plan.dateTerminated
&& this.hero.purchased.plan.dateTerminated !== '';
},
},
};
</script>

View File

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

View File

@@ -8,6 +8,13 @@
>
{{ $t('adminPanel') }}
</router-link>
<router-link
v-if="hasPermission(user, 'groupSupport')"
class="nav-link"
:to="{name: 'groupAdmin'}"
>
{{ $t('groupAdmin') }}
</router-link>
<router-link
v-if="hasPermission(user, 'accessControl')"
class="nav-link"

View File

@@ -0,0 +1,47 @@
<template>
<div class="form-group row">
<label class="col-sm-3 col-form-label"><slot name="label">{{ label }}</slot></label>
<div class="col-sm-9">
<slot>
<textarea
v-if="inputType === 'textarea'"
:value="value"
class="form-control"
:rows="rows"
@input="$emit('input', $event.target.value)"
></textarea>
<input
v-else
:value="value"
class="form-control"
:type="inputType"
@input="$emit('input', $event.target.value)"
>
</slot>
</div>
</div>
</template>
<script>
export default {
model: {
prop: 'value',
event: 'input',
},
props: {
label: {
type: String,
},
value: {
type: [String, Boolean],
},
inputType: {
type: String,
default: 'text',
},
rows: {
default: 3,
},
},
};
</script>

View File

@@ -0,0 +1,45 @@
<template>
<form>
<form-row
v-model="group.name"
:label="$t('groupName')"
/>
<form-row
v-model="group.summary"
:label="$t('guildSummary')"
input-type="textarea"
/>
<form-row
v-model="group.description"
:label="$t('groupDescription')"
input-type="textarea"
rows="6"
/>
<form-row
v-model="group.bannedWordsAllowed"
:label="$t('bannedWordsAllowed')"
input-type="checkbox"
/>
<form-row
v-model="group.leaderOnly.challenges"
:label="$t('leaderOnlyChallenges')"
input-type="checkbox"
/>
</form>
</template>
<script>
import formRow from '@/components/admin/formRow.vue';
export default {
components: {
formRow,
},
props: {
group: {
type: Object,
required: true,
},
},
};
</script>

View File

@@ -0,0 +1,69 @@
<template>
<div v-if="hasPermission(user, 'groupSupport')">
<h2>{{ group.name }}</h2>
<supportContainer
:title="$t('groupData')"
>
<groupData
:group="group"
/>
</supportContainer>
<supportContainer
:title="$t('groupPlanSubscription')"
/>
<supportContainer
v-if="group.type === 'party'"
:title="$t('questDetails')"
/>
<supportContainer
:title="$t('members')"
>
<members
:group="group"
/>
</supportContainer>
</div>
</template>
<script>
import { userStateMixin } from '../../../../mixins/userState';
import supportContainer from '../../supportContainer.vue';
import groupData from './groupData.vue';
import members from './members.vue';
export default {
components: {
supportContainer,
groupData,
members,
},
mixins: [userStateMixin],
data () {
return {
groupId: '',
group: {},
};
},
watch: {
groupId () {
this.loadGroup(this.groupId);
},
},
mounted () {
this.groupId = this.$route.params.groupId;
},
methods: {
clearData () {
this.group = {};
},
async loadGroup (groupId) {
this.$emit('changeGroupId', groupId);
this.group = await this.$store.dispatch('admin:getGroup', { groupId });
},
async updateGroup () {
await this.$store.dispatch('admin:updateGroup', { group: this.group });
this.$emit('groupSaved', this.group);
},
},
};
</script>

View File

@@ -0,0 +1,29 @@
<template>
<form-row
:label="$t('groupLeader')"
>
<strong class="col-form-label">
<router-link
:to="{'name': 'adminPanelUser', 'params': {'userIdentifier': group.leader }}"
>
{{ group.leader }}
</router-link>
</strong>
</form-row>
</template>
<script>
import formRow from '@/components/admin/formRow.vue';
export default {
components: {
formRow,
},
props: {
group: {
type: Object,
required: true,
},
},
};
</script>

View File

@@ -0,0 +1,93 @@
<template>
<div class="row standard-page col-12 d-flex justify-content-center">
<div class="group-admin-content">
<h1>{{ $t("groupAdmin") }}</h1>
<form
class="form-inline"
@submit.prevent="loadGroup(groupID)"
>
<div class="input-group col pl-0 pr-0">
<input
v-model="groupID"
class="form-control"
type="text"
placeholder="Group ID"
>
<div class="input-group-append">
<button
class="btn btn-primary"
type="button"
:disabled="!groupID"
@click="loadGroup(groupID)"
>
Load
</button>
</div>
</div>
</form>
<router-view
class="mt-3"
@changeGroupId="changeGroupId"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.uidField {
min-width: 45ch;
}
.input-group-append {
width:auto;
}
.group-admin-content {
flex: 0 0 800px;
max-width: 800px;
}
</style>
<script>
import VueRouter from 'vue-router';
import { mapState } from '@/libs/store';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
data () {
return {
groupID: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('groupAdmin'),
});
},
methods: {
changeGroupId (id) {
this.groupID = id;
},
async loadGroup (groupId) {
if (this.$router.currentRoute.name === 'groupAdminGroup') {
await this.$router.push({
name: 'groupAdmin',
});
}
await this.$router.push({
name: 'groupAdminGroup',
params: { groupId },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
this.$router.go();
}
});
},
},
};
</script>

View File

@@ -0,0 +1,53 @@
<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
<slot name="title">
{{ title }}
</slot>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<slot></slot>
</div>
<div
v-if="expand && onSave"
class="card-footer"
>
<button
class="btn btn-primary mt-1"
@click="onSave"
>
{{ $t('save') }}
</button>
</div>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
required: false,
},
onSave: {
type: Function,
required: false,
},
},
data () {
return {
expand: false,
};
},
};
</script>

View File

@@ -43,9 +43,11 @@ export default {
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
const parseSettings = JSON.parse(AUTH_SETTINGS);
const userId = parseSettings ? parseSettings.auth.apiId : '';
const username = this.$store?.state?.user?.data?.auth?.local?.username || '';
return this.$t('accountSuspended', {
userId,
username,
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
});
},

View File

@@ -7,7 +7,6 @@
@update-challenge="updateChallenge"
/>
<close-challenge-modal
:members="members"
:challenge-id="challenge._id"
:prize="challenge.prize"
:flag-count="challenge.flagCount"
@@ -72,32 +71,40 @@
</div>
<div class="col-12 col-md-6 text-right">
<div
class="box member-count"
class="box member-count p-2"
@click="showMemberModal()"
>
<div
class="svg-icon member-icon"
v-html="icons.memberIcon"
></div>
{{ challenge.memberCount }}
<div
v-once
class="details"
>
{{ $t('participantsTitle') }}
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon member-icon"
v-html="icons.memberIcon"
></div>
<span class="number">{{ challenge.memberCount }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('participantsTitle') }}
</div>
</div>
</div>
<div class="box">
<div
class="svg-icon gem-icon"
v-html="icons.gemIcon"
></div>
{{ challenge.prize || 0 }}
<div
v-once
class="details"
>
{{ $t('prize') }}
<div class="box prize-count p-2">
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon gem-icon"
v-html="icons.gemIcon"
></div>
<span class="number">{{ challenge.prize || 0 }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('prize') }}
</div>
</div>
</div>
</div>
@@ -304,7 +311,6 @@
.box {
display: inline-block;
padding: 1em;
border-radius: 2px;
background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
@@ -314,22 +320,88 @@
text-align: center;
font-size: 20px;
vertical-align: bottom;
overflow: hidden;
position: relative;
&.member-count:hover {
cursor: pointer;
}
.box-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.icon-number-row {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 0.1em;
.number {
font-size: 20px;
font-weight: normal;
margin-left: 0.2em;
}
}
.svg-icon {
width: 30px;
display: inline-block;
margin-right: .2em;
vertical-align: bottom;
}
.details {
font-size: 12px;
margin-top: 0.4em;
color: $gray-200;
width: 100%;
padding: 0 4px;
line-height: 1.15;
word-break: break-word;
max-height: 2.3em;
overflow: visible;
}
&.member-count {
.icon-number-row {
.svg-icon {
width: 24px;
height: 24px;
}
.number {
font-size: 18px;
}
}
.details {
font-size: 11px;
line-height: 1.1;
max-height: 2.2em;
}
}
&.prize-count {
.icon-number-row {
.svg-icon {
width: 24px;
height: 24px;
}
.number {
font-size: 18px;
}
}
.details {
font-size: 11px;
line-height: 1.1;
max-height: 2.2em;
}
}
}
@@ -624,7 +696,6 @@ export default {
this.members = [];
},
closeChallenge () {
this.initialMembersLoad();
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
},
edit () {

View File

@@ -4,6 +4,7 @@
id="close-challenge-modal"
:title="$t('endChallenge')"
size="md"
:hide-header="false"
>
<div
slot="modal-header"
@@ -15,6 +16,9 @@
>
{{ $t('endChallenge') }}
</h2>
<close-x
@close="$root.$emit('bv::hide::modal', 'close-challenge-modal')"
/>
</div>
<div class="row text-center">
<span
@@ -28,28 +32,67 @@
class="col-12"
>
<div class="col-12">
<div class="support-habitica">
<!-- @TODO: Add challenge achievement badge here-->
<div class="badge-section">
<div
class="gems-left"
v-html="icons.gemsOrange"
></div>
<div
class="challenge-badge"
v-html="icons.endChallengeBadge"
></div>
<div
class="gems-right"
v-html="icons.gemsPurple"
></div>
</div>
</div>
<div class="col-12">
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
</div>
<div class="col-12">
<member-search-dropdown
:text="winnerText"
:members="members"
:challenge-id="challengeId"
@member-selected="selectMember"
/>
<div class="col-12 search-input-container">
<div class="search-input-wrapper">
<div
class="search-icon"
v-html="icons.search"
></div>
<input
v-model="searchTerm"
class="search-input"
type="text"
placeholder="@Username"
@input="searchMembers"
@focus="showResults = true"
@blur="handleBlur"
>
<div
v-if="showResults && filteredMembers.length > 0"
class="search-results"
>
<div
v-for="member in filteredMembers"
:key="member._id"
class="search-result-item"
@mousedown="selectMember(member)"
>
{{ getMemberDisplayName(member) }}
</div>
</div>
</div>
</div>
<div class="col-12">
<button
v-once
class="btn btn-primary"
class="btn award-winner-btn"
:class="{'has-winner': winner._id}"
:disabled="!winner._id"
@click="closeChallenge"
>
{{ $t('awardWinners') }}
<span>{{ $t('awardWinners') }}</span>
<div
class="gem-icon"
v-html="icons.gem"
></div>
<span>{{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}</span>
</button>
</div>
</span>
@@ -60,14 +103,27 @@
</div>
</div>
<div class="col-12">
<strong v-once>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
<strong
v-once
class="delete-challenge-text"
>{{ $t('doYouWantedToDeleteChallenge') }}</strong>
</div>
<div
v-once
class="col-12 refund-text"
>
{{ $t('deleteChallengeRefundDescription') }}
</div>
<div class="col-12">
<button
v-once
class="btn btn-danger"
class="btn btn-danger delete-challenge-btn"
@click="deleteChallenge()"
>
<div
class="svg-icon color delete-icon"
v-html="icons.deleteIcon"
></div>
{{ $t('deleteChallenge') }}
</button>
</div>
@@ -82,6 +138,7 @@
<style lang='scss'>
@import '@/assets/scss/colors.scss';
@import '@/assets/scss/button.scss';
#close-challenge-modal {
h2 {
@@ -94,26 +151,190 @@
.header-wrap {
width: 100%;
padding-top: 2em;
padding-top: 32px;
position: relative;
}
.support-habitica {
background-image: url('@/assets/svg/for-css/support-habitica-gems.svg?raw');
width: 325px;
height: 89px;
.modal-close {
position: absolute;
right: 16px;
top: 16px;
padding: 0;
margin: 0;
}
.search-input-container {
margin-top: 1em !important;
}
.search-input-wrapper {
position: relative;
width: 384px;
margin: 0 auto;
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-55%);
width: 16px;
height: 16px;
color: $gray-200;
pointer-events: none;
display: flex;
align-items: center;
}
.search-input {
width: 100%;
height: 32px;
padding-left: 36px;
padding-right: 12px;
border: 1px solid $gray-400;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s ease, border-width 0.2s ease;
&:focus {
outline: none;
border: 2px solid $purple-400;
}
&::placeholder {
color: $gray-300;
}
}
.search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: $white;
border: 1px solid $gray-400;
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.search-result-item {
padding: 8px 16px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
&:hover {
background-color: $purple-600;
color: $purple-300;
}
}
}
}
.delete-challenge-text {
color: $maroon-50;
}
.refund-text {
font-family: 'Roboto', sans-serif;
font-size: 14px;
line-height: 24px;
font-weight: 400;
color: $gray-50;
margin-top: 0.5em !important;
}
.delete-challenge-btn {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 24px;
display: inline-flex;
align-items: center;
gap: 8px;
.delete-icon {
width: 16px;
height: 16px;
display: inline-flex;
}
}
.award-winner-btn {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 4px 12px;
transition: all 0.2s ease;
&:not(:disabled) {
background-color: $white;
color: $gray-200;
border: 1px solid $gray-400;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
&.has-winner {
background-color: $purple-200;
color: $white;
border-color: $purple-200;
}
&:hover:not(.has-winner) {
background-color: $gray-700;
}
}
.gem-icon {
width: 20px;
height: 20px;
display: inline-flex;
align-items: center;
color: $gems-color;
}
}
.badge-section {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin: -24px auto 0;
padding: 0.5rem 0;
.gems-left, .gems-right {
width: 64px;
height: 64px;
flex-shrink: 0;
}
.challenge-badge {
width: 48px;
height: 52px;
flex-shrink: 0;
}
}
.modal-footer, .modal-header {
border: none !important;
}
.modal-header {
padding: 0 !important;
}
.footer-wrap {
display: none;
}
.col-12 {
margin-top: 2em;
margin-top: 1.5em;
}
.col-12:first-child {
margin-top: 0;
}
.or {
@@ -123,21 +344,41 @@
margin-right: auto;
margin-left: auto;
font-weight: bold;
color: $gray-100;
}
}
</style>
<script>
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
import debounce from 'lodash/debounce';
import searchIcon from '@/assets/svg/for-css/search.svg?raw';
import deleteIcon from '@/assets/svg/delete.svg?raw';
import gemIcon from '@/assets/svg/gem.svg?raw';
import endChallengeBadge from '@/assets/svg/for-css/end_challenge_badge.svg?raw';
import gemsOrange from '@/assets/svg/for-css/orange100_red100_yellow100_gems.svg?raw';
import gemsPurple from '@/assets/svg/for-css/purple200_green10_blue100_gems.svg?raw';
import closeX from '@/components/ui/closeX';
export default {
components: {
memberSearchDropdown,
closeX,
},
props: ['challengeId', 'members', 'prize', 'flagCount'],
props: ['challengeId', 'prize', 'flagCount'],
data () {
return {
winner: {},
searchTerm: '',
showResults: false,
filteredMembers: [],
isSearching: false,
icons: Object.freeze({
search: searchIcon,
deleteIcon,
gem: gemIcon,
endChallengeBadge,
gemsOrange,
gemsPurple,
}),
};
},
computed: {
@@ -149,9 +390,58 @@ export default {
return this.flagCount > 0;
},
},
created () {
this.searchMembersDebounced = debounce(this.performSearch, 500);
},
methods: {
searchMembers () {
if (!this.searchTerm) {
this.filteredMembers = [];
this.isSearching = false;
return;
}
this.isSearching = true;
this.searchMembersDebounced();
},
async performSearch () {
if (!this.searchTerm) {
this.filteredMembers = [];
this.isSearching = false;
return;
}
const searchTerm = this.searchTerm.replace('@', '');
try {
const members = await this.$store.dispatch('members:getChallengeMembers', {
challengeId: this.challengeId,
searchTerm,
includeAllPublicFields: true,
});
this.filteredMembers = members.slice(0, 10);
} catch (err) {
this.filteredMembers = [];
} finally {
this.isSearching = false;
}
},
getMemberDisplayName (member) {
if (member.auth?.local?.username) {
return `@${member.auth.local.username}`;
}
return member.profile?.name || '';
},
selectMember (member) {
this.winner = member;
this.searchTerm = this.getMemberDisplayName(member);
this.showResults = false;
},
handleBlur () {
setTimeout(() => {
this.showResults = false;
}, 200);
},
async closeChallenge () {
this.challenge = await this.$store.dispatch('challenges:selectChallengeWinner', {

View File

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

View File

@@ -218,13 +218,19 @@
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
// somehow the browser felt like setting this 398px instead
// now its fixed to 400 :)
width: 400px;
max-width: 400px;
width: 100%;
margin-bottom: 1.5rem;
@media (max-width: 589px) {
max-width: 100%;
justify-content: center;
}
@media (max-width: 353px) {
gap: 0.25rem;
}
.quest-col {
::v-deep {
.item-wrapper {
@@ -251,6 +257,28 @@
::v-deep & {
.modal-dialog {
width: 448px !important;
max-width: calc(100vw - 20px);
margin: 0.5rem auto;
display: flex;
@media (max-width: 468px) {
width: 100% !important;
}
@media (max-width: 353px) {
width: 100% !important;
margin: 0.25rem auto;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -49,6 +49,12 @@
v-if="showOnboardingGuide"
:never-seen="hasSpecialBadge"
/>
<gift-one-get-one-notification
v-if="shouldShowG1g1"
:notification="g1g1Notification"
:event-key="g1g1EventKey"
@notification-removed="handleG1g1Removed"
/>
<component
:is="notification.type"
v-for="notification in notifications"
@@ -114,6 +120,7 @@
<script>
import * as quests from '@/../../common/script/content/quests';
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
import find from 'lodash/find';
import { mapState, mapActions } from '@/libs/store';
import notificationsIcon from '@/assets/svg/notifications.svg?raw';
import MenuDropdown from '../ui/customMenuDropdown';
@@ -151,6 +158,7 @@ export default {
CARD_RECEIVED,
CHALLENGE_INVITATION,
GIFT_ONE_GET_ONE,
GiftOneGetOneNotification: GIFT_ONE_GET_ONE,
GROUP_TASK_ASSIGNED,
GROUP_TASK_CLAIMED,
GROUP_TASK_NEEDS_WORK,
@@ -178,17 +186,14 @@ export default {
hasSpecialBadge: false,
quests,
openStatus: undefined,
g1g1Hidden: false,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
// NOTE: Those not listed here won't be shown in the notification panel!
handledNotifications: [
'NEW_STUFF',
'ITEM_RECEIVED',
'GIFT_ONE_GET_ONE',
'GROUP_TASK_NEEDS_WORK',
'GUILD_INVITATION',
'PARTY_INVITATION',
@@ -207,7 +212,10 @@ export default {
};
},
computed: {
...mapState({ user: 'user.data' }),
...mapState({
user: 'user.data',
currentEventList: 'worldState.data.currentEventList',
}),
notificationsOrder () {
// Returns a map of NOTIFICATION_TYPE -> POSITION
const orderMap = {};
@@ -286,9 +294,9 @@ export default {
return notifications;
},
// The total number of notification, shown inside the dropdown
notificationsCount () {
return this.notifications.length;
const g1g1Count = this.shouldShowG1g1 ? 1 : 0;
return this.notifications.length + g1g1Count;
},
hasUnseenNotifications () {
return this.notifications.some(notification => (notification.seen === false));
@@ -299,6 +307,30 @@ export default {
showOnboardingGuide () {
return !hasCompletedOnboarding(this.user);
},
currentG1g1Event () {
return find(this.currentEventList, event => event.promo === 'g1g1');
},
g1g1EventKey () {
if (!this.currentG1g1Event || !this.currentG1g1Event.start) return null;
const startDate = new Date(this.currentG1g1Event.start);
return `${startDate.getFullYear()}-${startDate.getMonth()}`;
},
shouldShowG1g1 () {
if (!this.currentG1g1Event) return false;
const eventKey = this.g1g1EventKey;
if (eventKey && window.sessionStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
return false;
}
return !this.g1g1Hidden;
},
g1g1Notification () {
return {
type: 'GIFT_ONE_GET_ONE',
id: `g1g1-event-${this.currentG1g1Event?.start || 'default'}`,
data: {},
seen: false,
};
},
},
mounted () {
const onboardingPanelState = getLocalSetting(CONSTANTS.keyConstants.ONBOARDING_PANEL_STATE);
@@ -364,6 +396,9 @@ export default {
isActionable (notification) {
return this.actionableNotifications.indexOf(notification.type) !== -1;
},
handleG1g1Removed () {
this.g1g1Hidden = true;
},
},
};

View File

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

View File

@@ -454,17 +454,14 @@ export default {
},
isUserMentioned () {
const message = this.msg;
if (message.highlight) {
return true;
}
const { user } = this;
const displayName = user.profile.name;
const { username } = user.auth.local;
const pattern = `@(${escapeRegExp(displayName)}|${escapeRegExp(username)})(\\b)`;
message.highlight = new RegExp(pattern, 'i').test(message.text);
if (!username) return false;
const usernamePattern = new RegExp(`@${escapeRegExp(username)}(?:\\b|(?=[^a-zA-Z0-9_]))`, 'i');
message.highlight = usernamePattern.test(message.text);
return message.highlight;
},
flagCountDescription () {

View File

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

View File

@@ -12,6 +12,12 @@
box-shadow: 0 1px 2px 0 rgba($black, 0.2);
z-index: 9;
height: 3rem;
flex-wrap: wrap;
@media (max-width: 683px) {
height: auto;
min-height: 3rem;
}
}
.nav-link {
@@ -23,6 +29,19 @@
padding: 0.75rem;
color: $gray-50;
white-space: nowrap;
@media (max-width: 683px) {
padding: 0.5rem;
font-size: 13px;
flex: 1 1 auto;
min-width: fit-content;
}
@media (max-width: 576px) {
padding: 0.5rem 0.4rem;
font-size: 12px;
}
&.active {
color: $purple-300;

View File

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

View File

@@ -269,7 +269,13 @@
.modal-dialog {
width: 448px;
max-width: calc(100vw - 20px);
box-sizing: border-box;
display: flex;
@media (max-width: 468px) {
width: 100%;
}
}
.badge-dialog {
@@ -346,7 +352,23 @@
.content {
text-align: center;
width: 448px;
width: 100%;
max-width: 448px;
margin: 0 auto;
@media (max-width: 468px) {
max-width: 100%;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.item-wrapper {
@@ -564,7 +586,7 @@
.limitedTime {
height: 32px;
width: 446px;
width: 100%;
font-size: 0.75rem;
margin: 24px 0 0 0;
background-color: $purple-300;
@@ -851,7 +873,7 @@ export default {
return;
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.purchased(this.item.text);
}
}

View File

@@ -111,6 +111,22 @@
.modal-dialog {
width: 448px;
max-width: calc(100vw - 20px);
display: flex;
@media (max-width: 468px) {
width: 100%;
}
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.modal-body {

View File

@@ -163,8 +163,33 @@
}
.modal-dialog {
margin-top: 8%;
width: 448px !important;
max-width: calc(100vw - 20px);
display: flex;
@media (max-width: 468px) {
width: 100% !important;
margin: 3rem auto 0.5rem;
}
@media (max-width: 353px) {
margin: 2.5rem auto 0.25rem;
}
}
.badge-dialog {
left: -8px;
top: -8px;
}
.modal-content {
display: flex;
flex-direction: column;
width: 100%;
@media (max-width: 300px) {
border-radius: 0;
}
}
.content {

View File

@@ -120,9 +120,9 @@
>
<ul>
<li>
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
- Web Developer
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
({{ $t('commGuideOnGitHub', {gitHubName: 'phillipthelen'}) }})
- Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
@@ -133,10 +133,6 @@
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
- Art, Community Management, Many Hats
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
- Designer
@@ -146,8 +142,12 @@
- Mobile Designer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
- Mobile Developer
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Kalista'}) }}
- Web Developer
</li>
<li>
{{ $t('commGuideAKA', {habitName: 'fizself', realName: 'Hafiz'}) }}
- Developer
</li>
</ul>
<p v-html="$t('commGuidePara013')"></p>
@@ -156,7 +156,7 @@
<em>
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
Breadstrings, Megan, Blade, Daniel the Bard, deilann, shanaqui, Nakonana,
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
Dewines, Alys, Fox_town, MaybeSteveRogers, Cantras, and heyeilatan.
</em>
</p>
<h2 id="final">

View File

@@ -35,7 +35,7 @@
</button>
<button
class="btn btn-secondary d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -162,13 +162,13 @@
>
<div
class="habit-option-icon svg-icon no-transition"
:class="task.up ? '' : 'disabled'"
:class="task.up ? '' : 'icon-disabled'"
v-html="icons.positive"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.up ? cssClass('icon') : 'disabled'"
:class="task.up ? cssClass('icon') : 'label-disabled'"
>
{{ $t('positive') }}
</div>
@@ -188,13 +188,13 @@
>
<div
class="habit-option-icon no-transition svg-icon negative mx-auto"
:class="task.down ? '' : 'disabled'"
:class="task.down ? '' : 'icon-disabled'"
v-html="icons.negative"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.down ? cssClass('icon') : 'disabled'"
:class="task.down ? cssClass('icon') : 'label-disabled'"
>
{{ $t('negative') }}
</div>
@@ -382,6 +382,45 @@
</div>
</div>
</div>
<div
v-if="showStatAssignment"
class="stat-assignment option mt-3"
>
<div class="form-group row">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedStat') }}</label>
<div class="col-12">
<div class="stat-dropdown-container">
<select-list
:items="statOptions"
:value="task.attribute"
key-prop="key"
active-key-prop="key"
@select="task.attribute = $event.key"
>
<template #item="{ item, button }">
<div class="stat-option-content">
<span
class="stat-option-title"
:class="item.key"
>
{{ $t(item.label) }}
</span>
<span
v-if="!button"
class="stat-option-description"
>
{{ $t(item.description) }}
</span>
</div>
</template>
</select-list>
</div>
</div>
</div>
</div>
<div
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
@@ -592,7 +631,7 @@
<button
class="btn btn-primary btn-footer
d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -881,12 +920,14 @@
}
}
.disabled {
.btn-disabled {
background-color: $white;
border: 2px solid transparent;
color: $gray-200;
line-height: 1.714;
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
cursor: not-allowed;
opacity: 0.6;
&:focus {
background-color: $white;
@@ -909,6 +950,87 @@
.streak-addon path {
fill: $gray-200;
}
.stat-dropdown-container {
.select-list {
.selectListItem {
margin-bottom: 0;
&:last-child {
margin-bottom: 0;
}
}
.selectListItem .dropdown-item {
padding: 8px 16px !important;
height: auto !important;
white-space: normal;
word-wrap: break-word;
&:hover,
&:focus {
background-color: rgba($purple-600, 0.25) !important;
}
}
.dropdown-toggle {
display: flex;
align-items: center;
.stat-option-content {
display: flex;
align-items: center;
.stat-option-title {
font-weight: normal;
color: $gray-50;
margin-bottom: 0;
}
}
}
}
.stat-option-content {
display: block;
width: 100%;
.stat-option-title {
display: block;
font-family: Roboto;
font-weight: 700;
font-size: 14px;
line-height: 1.71;
text-transform: capitalize;
margin-bottom: 4px;
&.str {
color: $maroon-100;
}
&.int {
color: $blue-50;
}
&.con {
color: $yellow-5;
}
&.per {
color: $purple-300;
}
}
.stat-option-description {
display: block;
font-family: Roboto;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
margin-bottom: 0;
}
}
}
</style>
<style lang="scss" scoped>
@@ -948,7 +1070,7 @@
height: 10px;
color: $white;
&.disabled {
&.icon-disabled {
color: $gray-200;
}
@@ -962,7 +1084,7 @@
font-weight: bold;
text-align: center;
&.disabled {
&.label-disabled {
color: $gray-100;
font-weight: normal;
}
@@ -1018,10 +1140,9 @@
border: 0;
}
.disabled .input-group-text {
.input-group-outer.disabled .input-group-text {
color: $gray-200;
}
</style>
<script>
@@ -1036,6 +1157,7 @@ import SelectMulti from './modal-controls/selectMulti';
import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty';
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import selectList from '@/components/ui/selectList';
import syncTask from '../../mixins/syncTask';
@@ -1059,6 +1181,7 @@ export default {
selectTranslatedArray,
toggleCheckbox,
lockableLabel,
selectList,
},
directives: {
markdown: markdownDirective,
@@ -1092,6 +1215,12 @@ export default {
con: 'constitution',
per: 'perception',
},
statOptions: [
{ key: 'str', label: 'strength', description: 'strTaskText' },
{ key: 'int', label: 'intelligence', description: 'intTaskText' },
{ key: 'con', label: 'constitution', description: 'conTaskText' },
{ key: 'per', label: 'perception', description: 'perTaskText' },
],
calendarHighlights: { dates: [new Date()] },
};
},
@@ -1185,6 +1314,12 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
showStatAssignment () {
return this.task.type !== 'reward'
&& !this.groupId
&& this.user.preferences.automaticAllocation === true
&& this.user.preferences.allocationMode === 'taskbased';
},
},
watch: {
task () {

View File

@@ -12,7 +12,7 @@
<template #button-content>
<slot
name="item"
:item="selected || placeholder"
:item="selectedItem || placeholder"
:button="true"
>
<!-- Fallback content -->
@@ -134,6 +134,14 @@ export default {
}),
};
},
computed: {
selectedItem () {
if (this.activeKeyProp) {
return this.items.find(item => item[this.activeKeyProp] === this.selected);
}
return this.items.find(item => item === this.selected);
},
},
methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item.key || item.identifier;

View File

@@ -116,7 +116,7 @@
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $green-10;
background-color: $green-50;
}
.toggle-switch-inner:after {

View File

@@ -1126,7 +1126,12 @@ export default {
this.loadUser();
this.oldTitle = this.$store.state.title;
this.handleExternalLinks();
this.selectPage(this.startingPage);
// Check if there's a hash in the URL to determine the starting page
let pageToSelect = this.startingPage;
if (window.location.hash && (window.location.hash === '#stats' || window.location.hash === '#achievements')) {
pageToSelect = window.location.hash.substring(1);
}
this.selectPage(pageToSelect);
this.$root.$on('habitica:report-profile-result', () => {
this.loadUser();
});
@@ -1211,10 +1216,15 @@ export default {
},
selectPage (page) {
this.selectedPage = page || 'profile';
window.history.replaceState(null, null, '');
const profileUserId = this.userId || this.userLoggedIn._id;
let newPath = `/profile/${profileUserId}`;
if (page !== 'profile') {
newPath += `#${page}`;
}
window.history.replaceState(null, null, newPath);
this.$store.dispatch('common:setTitle', {
section: this.$t('user'),
subSection: this.$t(this.startingPage),
subSection: this.$t(page),
});
},
getNextIncentive () {

File diff suppressed because it is too large Load Diff

View File

@@ -3,14 +3,10 @@ import isEqual from 'lodash/isEqual';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import amplitude from 'amplitude-js';
import { gtag, install } from 'ga-gtag';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const DEBUG_ENABLED = import.meta.env.DEBUG_ENABLED === 'true';
const GA_ID = import.meta.env.GA_ID;
const IS_PRODUCTION = import.meta.env.NODE_ENV === 'production';
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
@@ -19,7 +15,7 @@ let analyticsReady = false;
function _getConsentedUser () {
const store = getStore();
const user = store.state.user.data;
if (!user?.preferences?.analyticsConsent || navigator.globalPrivacyControl) {
if (!user?.preferences?.analyticsConsent) {
return false;
}
return user;
@@ -69,10 +65,6 @@ function _gatherUserStats (properties) {
export function safeSetup (userId) {
if (analyticsLoading || analyticsReady) return;
analyticsLoading = true;
install(GA_ID, {
debug_mode: DEBUG_ENABLED || !IS_PRODUCTION,
user_id: userId,
});
amplitude.getInstance().init(AMPLITUDE_KEY, userId);
analyticsReady = true;
analyticsLoading = false;
@@ -90,7 +82,6 @@ export function track (properties, options = {}) {
// Track events on the server by default
if (trackOnClient === true) {
amplitude.getInstance().logEvent(properties.eventAction, properties);
gtag('event', properties.eventAction, properties);
} else {
const store = getStore();
store.dispatch('analytics:trackEvent', properties);
@@ -105,7 +96,6 @@ export function updateUser (properties = {}) {
// Use nextTick to avoid blocking the UI
Vue.nextTick(() => {
_gatherUserStats(properties);
gtag('set', 'user_properties', properties);
forEach(properties, (value, key) => {
const identify = new amplitude.Identify().set(key, value);
amplitude.getInstance().identify(identify);

View File

@@ -1,7 +1,16 @@
import habiticaMarkdown from 'habitica-markdown/withMentions';
import escapeRegExp from 'lodash/escapeRegExp';
export default function renderWithMentions (text, user) {
if (!text) return null;
const env = { userName: user.auth.local.username, displayName: user.profile.name };
return habiticaMarkdown.render(String(text), env);
const env = { userName: user.auth.local.username };
let html = habiticaMarkdown.render(String(text), env);
if (user.auth.local.username) {
const username = escapeRegExp(user.auth.local.username);
const regex = new RegExp(`(<span class="at-text">@)(${username})(</span>)`, 'gi');
html = html.replace(regex, (match, p1, p2, p3) => `${p1.replace('at-text', 'at-text at-highlight')}${p2}${p3}`);
}
return html;
}

View File

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

View File

@@ -98,7 +98,7 @@
}
.settings-content {
flex: 0 0 732px;
flex: 0 0 751px;
max-width: unset;
::v-deep {

View File

@@ -33,6 +33,21 @@
v-html="$t('privacySettingsOverview') + ' ' + $t('learnMorePrivacy')"
>
</p>
<div
v-if="gpcEnabled"
class="mx-4 px-3 py-2 mb-4 gpc-alert d-flex align-items-center black bg-yellow-50"
>
<div
class="svg svg-icon mr-2"
v-html="icons.alert"
>
</div>
<div
class="gpc-message"
v-html="gpcInfo"
>
</div>
</div>
<div
class="d-flex justify-content-center"
>
@@ -91,6 +106,29 @@
line-height: 1.33;
}
.gpc-alert {
border-radius: 4px;
line-height: 1.714;
.gpc-message {
opacity: 0.9;
}
::v-deep a {
color: $black;
text-decoration: underline;
}
.svg-icon {
width: 16px;
opacity: 0.75;
::v-deep svg path {
fill: $black;
}
}
}
.mb-28p {
margin-bottom: 28px;
}
@@ -110,6 +148,7 @@ import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import { mapState } from '@/libs/store';
import alert from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [
@@ -120,14 +159,32 @@ export default {
SaveCancelButtons,
ToggleSwitch,
},
data () {
return {
icons: Object.freeze({
alert,
}),
};
},
computed: {
...mapState({
user: 'user.data',
}),
gpcEnabled () {
return navigator.globalPrivacyControl;
},
gpcInfo () {
const gpcUrl = 'https://globalprivacycontrol.org/';
if (this.user.preferences.analyticsConsent) {
return this.$t('gpcPlusAnalytics', { url: gpcUrl });
}
return this.$t('gpcWarning', { url: gpcUrl });
},
},
methods: {
finalize () {
this.setUserPreference('analyticsConsent');
localStorage.setItem('analyticsConsent', this.user.preferences.analyticsConsent);
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
},
prefToggled () {
@@ -135,7 +192,10 @@ export default {
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = newVal;
},
resetControls () {
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
}
},
},
};

View File

@@ -263,11 +263,12 @@ export default {
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
const analyticsConsent = localStorage.getItem('analyticsConsent');
if (analyticsConsent !== null
&& analyticsConsent !== this.user.preferences.analyticsConsent
) {
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
let analyticsConsent = localStorage.getItem('analyticsConsent');
if (analyticsConsent !== null) {
analyticsConsent = analyticsConsent === 'true';
if (analyticsConsent !== this.user.preferences.analyticsConsent) {
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
}
}
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {

View File

@@ -24,6 +24,8 @@ const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
// Tasks
@@ -88,6 +90,9 @@ const router = new VueRouter({
path: '/profile/:userId',
props: true,
},
{ name: 'profile', path: '/user/profile' },
{ name: 'stats', path: '/user/stats' },
{ name: 'achievements', path: '/user/achievements' },
{
path: '/inventory',
component: InventoryContainer,
@@ -216,6 +221,28 @@ const router = new VueRouter({
},
],
},
{
name: 'groupAdmin',
path: 'groups',
component: GroupAdminPage,
meta: {
privilegeNeeded: [ // any one of these is enough to give access
'groupSupport',
],
},
children: [
{
name: 'groupAdminGroup',
path: ':groupId',
component: GroupAdminGroupPage,
meta: {
privilegeNeeded: [
'groupsSupport',
],
},
},
],
},
{
name: 'blockers',
path: 'blockers',
@@ -345,6 +372,10 @@ router.beforeEach(async (to, from, next) => {
if (to.params.startingPage !== undefined) {
startingPage = to.params.startingPage;
}
// Check if there's a hash in the URL for stats or achievements
if (to.hash === '#stats' || to.hash === '#achievements') {
startingPage = to.hash.substring(1);
}
if (from.name === null) {
store.state.postLoadModal = `profile/${to.params.userId}`;
return next({ name: 'tasks' });
@@ -365,10 +396,18 @@ router.beforeEach(async (to, from, next) => {
}
if ((to.name === 'stats' || to.name === 'achievements' || to.name === 'profile') && from.name !== null) {
const userId = store.state.user.data._id;
let redirectPath = `/profile/${userId}`;
if (to.name === 'stats') {
redirectPath += '#stats';
} else if (to.name === 'achievements') {
redirectPath += '#achievements';
}
router.app.$emit('habitica:show-profile', {
userId,
startingPage: to.name,
fromPath: from.path,
toPath: to.path,
toPath: redirectPath,
});
return null;
}

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
import axios from 'axios';
import { authAsCredentialsState, LOCALSTORAGE_AUTH_KEY } from '@/libs/auth';
const GA_ID = import.meta.env.GA_ID;
function saveLocalDataAuth (store, apiId, apiToken) {
const credentialsObj = {
auth: {
@@ -123,9 +121,6 @@ export async function appleAuth (store, params) {
export function logout (store, options = {}) {
localStorage.clear();
sessionStorage.clear();
if (window.gtag) {
window.gtag('config', GA_ID, { user_id: null });
}
const query = options.redirectToLogin === true ? '?redirectToLogin=true' : '';
window.location.href = `/logout-server${query}`;
}

View File

@@ -38,3 +38,9 @@ export async function getHeroGroupPlans (store, payload) {
const response = await axios.get(url);
return response.data.data;
}
export async function deleteHero (store, payload) {
const url = `/api/v4/members/${payload.uuid}?deleteAccount=${payload.deleteHabiticaAccount}&deleteAmplitude=${payload.deleteAmplitudeData}`;
const response = await axios.delete(url);
return response.data.data;
}

View File

@@ -1,6 +1,6 @@
import { flattenAndNamespace } from '@/libs/store/helpers/internals';
import * as adminPanel from './adminPanel';
import * as admin from './admin';
import * as common from './common';
import * as user from './user';
import * as tasks from './tasks';
@@ -26,7 +26,7 @@ import * as blockers from './blockers';
// Example: fetch in user.js -> 'user:fetch'
const actions = flattenAndNamespace({
adminPanel,
admin,
common,
user,
tasks,

View File

@@ -12,12 +12,12 @@ describe('renderWithMentions', () => {
expect(result).to.be.null;
});
test('highlights displayname', () => {
test('does not highlight displayname to prevent impersonation', () => {
const text = 'hello @displayedUser with text after';
const result = renderMarkdown(text, user('user', 'displayedUser'));
expect(result).to.contain('<span class="at-text at-highlight">@displayedUser</span>');
expect(result).to.contain('<span class="at-text">@displayedUser</span>');
expect(result).to.not.contain('<span class="at-text at-highlight">@displayedUser</span>');
});
test('highlights username', () => {
@@ -56,7 +56,8 @@ describe('renderWithMentions', () => {
const result = renderMarkdown(plainText, user('use', 'mentions'));
expect(result).to.contain('<span class="at-text at-highlight">@mentions</span>');
expect(result).to.contain('<span class="at-text">@mentions</span>');
expect(result).to.not.contain('<span class="at-text at-highlight">@mentions</span>');
expect(result).to.contain('<span class="at-text at-highlight">@use</span>');
expect(result).to.contain('<span class="at-text">@mail</span>');
expect(result).to.not.contain('<span class="at-text at-highlight">@mentions</span>.com');

View File

@@ -26,7 +26,6 @@ const envVars = [
'EMAILS_COMMUNITY_MANAGER_EMAIL',
'EMAILS_TECH_ASSISTANCE_EMAIL',
'EMAILS_PRESS_ENQUIRY_EMAIL',
'GA_ID',
'STRIPE_PUB_KEY',
'GOOGLE_CLIENT_ID',
'APPLE_AUTH_CLIENT_ID',
@@ -36,7 +35,7 @@ const envVars = [
'TIME_TRAVEL_ENABLED',
'DEBUG_ENABLED',
'CONTENT_SWITCHOVER_TIME_OFFSET',
// TODO necessary? if yes how not to mess up with vue cli? 'NODE_ENV'
'PLAY_CONSOLE_ORDERS_BASE_URL',
];
const envObject = {};

View File

@@ -108,5 +108,6 @@
"resetFlagCount": "Markierungszähler zurücksetzen",
"cannotClone": "Diese Herausforderung kann nicht dupliziert werden, weil einer oder mehrere Spieler sie als unangemessen gemeldet haben. Einer der Mitarbeiter wird dich in Kürze mit Anweisungen kontaktieren. Wenn mehr als 48 Stunden vergangen sind, und du nichts von ihnen gehört hast, schicke bitte eine Email an admin@habitica.com, um Unterstützung zu erhalten.",
"resetFlags": "Markierungen zurücksetzen",
"messageChallengeFlagOfficial": "Offizielle Herausforderungen können nicht gemeldet werden."
"messageChallengeFlagOfficial": "Offizielle Herausforderungen können nicht gemeldet werden.",
"deleteChallengeRefundDescription": "Wenn du diese Herausforderung löschst, bekommst du den Preis in Edelsteinen erstattet und die Aufgaben der Herausforderung verbleiben auf der Aufgabentafel der Teilnehmer."
}

View File

@@ -28,10 +28,10 @@
"marketing1Lead1Title": "Mache dein Leben zum Spiel",
"marketing1Lead1": "Habitica ist die perfekte App, für alle die Probleme mit ToDo-Listen haben. Wir verwenden bekannte Spiel-Mechaniken wie Belohnungen in Gold, XP und Gegenstände, die dir dabei helfen, dich produktiver zu fühlen und dein Erfolgserlebnis zu steigern, wenn du Aufgaben vollendest. Je besser Du Dich dabei anstellst, umso weiter kommst Du im Spiel.",
"marketing1Lead2Title": "Rüsten dich mit Stil aus",
"marketing1Lead2": "Sammele Schwerter, Rüstungen und vieles mehr mit Gold, welches du beim Vollenden von Aufgaben verdienst. Mit hunderten von Ausrüstungsstücken, die du sammeln und auswählen kannst, werden dir nie Kombinationen zum Ausprobieren ausgehen. Optimiere deine Statistik, deinen Style oder beides zusammen! ",
"marketing1Lead2": "Sammele Schwerter, Rüstungen und vieles mehr mit dem Gold, das du beim Vollenden von Aufgaben verdienst. Mit hunderten von Stücken zum Sammeln, aus denen du auswählen kannst, werden dir die Kombinationen zum Ausprobieren nie ausgehen. Optimiere deine Werte, deinen Style oder beides! ",
"marketing1Lead3Title": "Verdiene Belohnungen für deine Bemühungen",
"marketing1Lead3": "Etwas zu haben, auf das man sich freuen kann, mag den Unterschied ausmachen, ob man eine Aufgabe erledigt oder ob sie einen wochenlang quält. Wenn das Leben keine Belohnung bietet ist Habitica für dich da! Du wirst für jede Aufgabe belohnt, aber Überraschungen gibt es an jeder Ecke - also mach weiter so! ",
"marketing2Header": "Schließe dich mit Freunden zusammen",
"marketing2Header": "Verbünde dich mit Freunden",
"marketing2Lead1Title": "Soziale Produktivität",
"marketing2Lead1": "Hole dir einen Motivationsschub, indem du mit anderen zusammenarbeitest, konkurrierst und interagierst! Habitica wurde entwickelt, um den effektivsten Teil eines jeden Selbstverbesserungsprogramms zu nutzen: soziale Verantwortung.",
"marketing2Lead2Title": "Bekämpfe Monster in Quests",

View File

@@ -2215,9 +2215,9 @@
"armorSpecialWinter2021RogueText": "Efeu-Grünes Gewand",
"weaponSpecialWinter2021HealerNotes": "Dirigiere deine Kämpfe mit unvorhersehbarem Schwung, wie ein Schneegestöber! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021HealerText": "Flocken Flanken Rute",
"weaponSpecialWinter2021MageNotes": "Diese mächtige Waffe ist nicht nur eine Phase! Konzentriere deine Kräfte, fokussiere den Verlauf eines Monates und studiere den Lauf von Zeit und Raum. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021MageNotes": "Diese mächtige Waffe ist definitiv mehr als nur eine Phase! Konzentriere deine Kräfte, fokussiere auf den Verlauf des Monates und studiere den Lauf von Zeit und Raum. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021MageText": "Magischer Mond-Phaser",
"weaponSpecialWinter2021WarriorNotes": "Hiermit kannst Du die größten Frische an Land ziehen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021WarriorNotes": "Hiermit kannst Du die größten Fische an Land ziehen! Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021WarriorText": "Mächtige Angelrute",
"weaponSpecialWinter2021RogueNotes": "Tarnung und Waffe in einem, die giftigen Früchte der Stechpalme helfen dir mit den schwierigsten Aufgaben umzugehen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe 2020-2021 Winterausrüstung.",
"weaponSpecialWinter2021RogueText": "Ilex-Beeren Morgenstern",
@@ -2861,7 +2861,7 @@
"weaponSpecialSummer2024MageText": "Seeanemonen Zauberstab",
"weaponSpecialSummer2024MageNotes": "Diese grandiosen Tentakel können Magie gleichzeitig abhalten, ablenken und steuern. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
"weaponSpecialSummer2024HealerNotes": "Du wirst erstaunt sein zu entdecken, wie hart die Muschel am Ende des Stabs ist. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
"weaponArmoireCorsairsBladeNotes": "Ob du sie zum Plündern oder zum Schutz trägst - du kannst froh sein, daß du diese grimmige Klinge mit dir zur See gebracht hast. Achte nur darauf, daß sie sicher verstaut ist, wenn nicht in Benutzung. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Korsaren Set (Gegenstand 3 von 3)",
"weaponArmoireCorsairsBladeNotes": "Ob du sie zum Plündern oder zum Schutz trägst - du kannst froh sein, daß du diese scharfe Klinge mit dir zur See gebracht hast. Achte nur darauf, dass sie sicher verstaut ist, wenn nicht in Benutzung. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Piraten Set (Gegenstand 3 von 3).",
"weaponArmoireCorsairsBladeText": "Korsarenklinge",
"armorSpecialFall2023MageText": "Scharlachrote Hexenmeister Robe",
"armorSpecialFall2023MageNotes": "Mit scharlachrotem Garn und goldenen Akzenten ist dieses Outfit ein Wunder für die Sinne. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2023 Herbstausrüstung.",
@@ -2891,7 +2891,7 @@
"weaponMystery202408Text": "Arkanes Aegis",
"weaponMystery202408Notes": "Ein magisches Bläschen-Schild, das dich vor gegnerischen Zaubersprüchen schützt, oder dir hilft, im Wasser oder in der Luft zu schweben. Gewährt keinen Attributbonus. August 2024 Abonnentengegenstand.",
"weaponArmoireDragonKnightsLanceText": "Drachenritter Lanze",
"weaponArmoireDragonKnightsLanceNotes": "Diese rot-silberne Lanze hat so manchen Gegner aus dem Sattel seines Reittiers gehoben. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Drachenritter Set (Gegenstand 3 von 3)",
"weaponArmoireDragonKnightsLanceNotes": "Diese rot-silberne Lanze hat so manchen Gegner aus dem Sattel seines Reittiers gehoben. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Drachenritter Set (Gegenstand 3 von 3).",
"armorSpecialSummer2024WarriorText": "Walhai Schwanzflosse",
"armorSpecialSummer2024WarriorNotes": "Nachdem du dich in einen echten Walhai Krieger verwandelt hast, schwimm kühn auf deine Aufgaben zu! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2024 Sommerausrüstung.",
"armorSpecialSummer2024MageText": "Seeanemonen Flosse",
@@ -2925,14 +2925,14 @@
"armorSpecialFall2024HealerNotes": "Sei eins mit der Galaxis und hypnotisiere Zuschauer mit dieser Rüstung. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe 2024 Herbstausrüstung.",
"armorSpecialFall2024MageText": "Unterwelt Hexer Rüstung",
"armorSpecialFall2024MageNotes": "Sei eins mit der Unterwelt und umarme die Macht der Magier, die vor dir diese Rüstung trugen. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe 2024 Herbstausrüstung.",
"weaponArmoireFunnyFoolBatonNotes": "Du kannst mit einem Schwung Deines Stabes eine Pointe vortragen, die Aufmerksamkeit erregen oder Beifall ernten. Erhöht Ausdauer und Stärke jeweils um <%= attrs %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 3 von 3)",
"weaponArmoireFunnyFoolBatonNotes": "Du kannst mit einem Schwung deines Stabes eine Pointe vortragen, Aufmerksamkeit erregen oder Beifall ernten. Erhöht Ausdauer und Stärke jeweils um <%= attrs %>. Verzauberter Schrank: Lustiges Narren-Set (Gegenstand 3 von 3).",
"armorArmoireTeaGownText": "Teekränzchen Kleid",
"armorArmoireTeaGownNotes": "Du bist zäh, kreativ, brilliant und so modisch! Erhöht Stärke und Intelligenz um jeweils <%= attrs %>. Verzauberter Schrank: Teekränzchen Set (Gegenstand 1 von 3).",
"armorMystery202401Text": "Verschneite Zauberer Roben",
"armorMystery202401Notes": "Diese Roben erscheinen filigran wie Schneeflocken, aber werden dich reichlich warm halten, während du deine winterliche Magie wirkst. Gewährt keinen Attributbonus. Januar 2024 Abonnentengegenstand.",
"armorMystery202406Notes": "Suche deine Feinde heim mit Stil und Flair! Gewährt keinen Attributbonus. Juni 2024 Abonnentengegenstand.",
"armorMystery202407Text": "Liebenswerter Axolotl Anzug",
"weaponArmoireSpookyCandyBucketNotes": "Mit einem epischen Kostüm wie diesem wirst du derart viel Süßigkeiten bekommen! Gut, dass du diesen bodenlosen Eimer hast, um das alles aufzunehmen. Versuch, nicht zu naschen, bevor du nach Hause kommst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Süßes oder Saures Set (Gegenstand 2 von 2)",
"weaponArmoireSpookyCandyBucketNotes": "Mit einem epischen Kostüm wie diesem wirst du so viele Süßigkeiten bekommen! Gut, dass du diesen Eimer ohne Boden hast, um das alles aufzunehmen. Versuch, nicht zu naschen, bevor du nach Hause kommst. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Süßes oder Saures Set (Gegenstand 2 von 2).",
"weaponArmoireSpookyCandyBucketText": "Grusliger Süßigkeiten Eimer",
"armorMystery202407Notes": "Gleite durch Seen und Kanäle mit deinem hin und her fegenden rosa Schwanz! Gewährt keinen Attributbonus. Juli 2024 Abonnentengegenstand.",
"armorArmoireJewelersApronText": "Juweliers-Schürze",
@@ -2957,7 +2957,7 @@
"armorArmoireKarateGiText": "Karategi",
"armorArmoireKarateGiNotes": "Diese leichte Karate-Uniform ist perfekt für Training oder Wettbewerbe. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Karate-Set (Gegenstand 1 von 10).",
"weaponArmoireStormKnightAxeText": "Axt des Sturmritters",
"weaponArmoireStormKnightAxeNotes": "Sammle deine Wut und schlage wie ein Donnerschlag zu! Erhöht Stärke um <%= str %>. Verzauberter Schrank: Sturmritter-Set (Gegenstand 3 von 3)",
"weaponArmoireStormKnightAxeNotes": "Sammle deine Wut und schlage wie ein Donnerschlag zu! Erhöht Stärke um <%= str %>. Verzauberter Schrank: Sturmritter-Set (Gegenstand 3 von 3).",
"armorArmoireDiagonalRainbowShirtNotes": "Ein Klecks Farbe mit einem Schuss Stil. Sei fröhlich! Erhöht Ausdauer und Wahrnehmung um jeweils <%= attrs %> . Verzauberter Schrank: Regenbogen-Set (Gegenstand 2 von 2).",
"armorArmoireAdmiralsUniformText": "Admirals-Uniform",
"armorArmoireAdmiralsUniformNotes": "Wir salutieren dir! Diese Marineuniform signalisiert, dass du bereit bist, das Kommando sowohl von deinen Aufgaben als auch von einem Schiff zu übernehmen. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Admirals-Set (Gegenstand 2 von 2).",
@@ -3315,7 +3315,7 @@
"headMystery202504Notes": "Trage diese mysteriöse Visage, um unentdeckt unter den oskursten Fabelwesen der Welt zu verweilen. Gewährt keinen Attributbonus. April 2025 Abonnentengegenstand.",
"headArmoireSillierBlueTophatNotes": "Etwas Klasse, etwas Raffinesse. Erhöht Stärke und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Noch Lächerlicherer Smoking Set (Gegenstand 2 von 2).",
"weaponArmoireGildedKnightsSpearText": "Vergoldeter Ritter Speer",
"weaponArmoireGildedKnightsSpearNotes": "Mit dieser Waffe kannst du sicherstellen, dass jeder immer seine Schulden bezahlt. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 3 von 3)",
"weaponArmoireGildedKnightsSpearNotes": "Mit dieser Waffe kannst du sicherstellen, dass jeder immer seine Schulden bezahlt. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 3 von 3).",
"armorArmoireGildedKnightsPlateNotes": "In dieser Rüstung bist du fast unbesiegbar. Deine Feinde werden dich sicher dröhnen hören! Erhöht Wahrnehmung um <%= per %>. Verzauberter Schrank: Vergoldeter Ritter Set (Gegenstand 2 von 3)",
"armorArmoireGildedKnightsPlateText": "Vergoldeter Ritter Rüstung",
"headArmoireGildedKnightsHelmText": "Vergoldeter Ritter Helm",
@@ -3330,7 +3330,7 @@
"weaponSpecialSummer2025RogueNotes": "Dieser Tentakel wird deine Ziele eng umklammern, so dass du den Schwung nicht verlierst, wenn du deine Aufgaben erledigst. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Sommerausrüstung 2025.",
"weaponSpecialSummer2025HealerText": "Ruderschnecken Flügelpaddel",
"weaponSpecialSummer2025HealerNotes": "Beschreibe rudernd einen Doppelkreis, während du dich vorwärts bewegst, und bei deinen Aufgaben tolle Fortschritte machst. Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Sommerausrüstung 2025.",
"weaponArmoireBeekeepersSmokerNotes": "Benutze dies, um deine Bienen zu beruhigen, damit du den Honig besser holen kannst. Die Bienen wird es nicht stören. Ehrlich, wir alle könnten von Zeit zu Zeit ein paar extra Minuten Ruhe gebrauchen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Imker Set (Gegenstand 3 von 4)",
"weaponArmoireBeekeepersSmokerNotes": "Benutze dies, um deine Bienen zu beruhigen, damit du den Honig besser holen kannst. Die Bienen wird es nicht stören. Um ehrlich zu sein, wir alle könnten von Zeit zu Zeit ein paar extra Minuten Ruhe gebrauchen. Erhöht Intelligenz um <%= int %>. Verzauberter Schrank: Imker Set (Gegenstand 3 von 4).",
"weaponArmoireBeekeepersSmokerText": "Smoker",
"armorSpecialSummer2025WarriorText": "Kammmuschel Rüstung",
"armorSpecialSummer2025WarriorNotes": "Diese Rüstung macht dich nicht nur robust, sondern auch schnell. Kampf oder Flucht - es ist deine Wahl! Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Sommerausrüstung 2025.",
@@ -3344,7 +3344,7 @@
"armorArmoireBeekeepersSuitNotes": "Schütze dich, während du nach deinen fleißigen Hummeln schaust. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Imker Set (Gegenstand 2 von 4)",
"headSpecialSummer2025RogueNotes": "Deine Sehkraft wird sich verbessern sobald Du diese Maske aufsetzt. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommer 2025 Ausrüstung.",
"weaponSpecialSummer2025MageText": "Zweigkoralle",
"weaponSpecialSummer2025MageNotes": "Verzweige dich mit deinen Talenten und Fähigkeiten, um eine Reihe von verschiedenen Aufgaben anzugreifen. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommerausrüstung 2025",
"weaponSpecialSummer2025MageNotes": "Verzweige dich mit deinen Talenten und Fähigkeiten, um eine Reihe von verschiedenen Aufgaben anzugreifen. Erhöht Intelligenz um <%= int %> und Wahrnehmung um <%= per %>. Limitierte Ausgabe Sommerausrüstung 2025.",
"headSpecialSummer2025WarriorText": "Kammmuschelhelm",
"headSpecialSummer2025WarriorNotes": "Undurchdringlich und perfekt zugespitzt wird dich dieser Helm sogar vor Seesternen beschützen. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Sommerausrüstung 2025.",
"headSpecialSummer2025RogueText": "Tintenfischmaske",
@@ -3386,5 +3386,58 @@
"weaponMystery202508Text": "Funkelnde Blutrote klinge",
"weaponMystery202508Notes": "Diese wirbelnde Klinge wird jedes Monster oder rote Tagesaufgabe, die deinen Weg kreuzen, in Schrecken versetzen! Gewährt keinen Attributbonus. August 2025 Abonnentengegenstand.",
"weaponMystery202511Text": "Frostschwert",
"weaponMystery202511Notes": "Der eisige Glanz dieses Schwertes wird selbst mit dunkelroten Aufgaben kurzen Prozess machen. Gewährt keinen Attributbonus. Novermber 2025 Abonnentengegenstand."
"weaponMystery202511Notes": "Der eisige Glanz dieses Schwertes wird selbst mit dunkelroten Aufgaben kurzen Prozess machen. Gewährt keinen Attributbonus. Novermber 2025 Abonnentengegenstand.",
"weaponArmoireBlacksmithsHammerText": "Schmiedehammer",
"weaponArmoireBlacksmithsHammerNotes": "Dieser Hammer ist für Metallarbeiten, er ist aber auch ideal geeignet inmitten von rotglühenden Kohlen und rotglühenden Tagesaufgaben. Erhöht Stärke um <%= str %>. Verzauberter Schrank: Schmiedeset (Gegenstand 3 von 3).",
"armorSpecialFall2025WarriorText": "Sasquatch Rüstung",
"armorSpecialFall2025WarriorNotes": "Weder deine großen Füße, noch dein großer Körper wird zu groß sein um in diese saisonale Rüstung zu passen. Erhöht Ausdauer um <%= con %>. Limitierte Herbstausrüstung 2025.",
"armorSpecialFall2025RogueText": "Skelett Rüstung",
"armorSpecialFall2025HealerText": "Kobold Rüstung",
"armorSpecialFall2025HealerNotes": "Diese saisonale Rüstung lässt dich mit den dunklen Wäldern verschmelzen um einen strategischen Rückzug anzutreten. Erhöht Ausdauer um <%= con %>. Limitierte Herbstausrüstung 2025.",
"armorSpecialFall2025MageText": "Maskierter Geist Rüstung",
"armorSpecialFall2025MageNotes": "Diese saisonale Rüstung wird körperlos sobald du sie anziehst. Erhöht Intelligenz um <%= int %>. Limitierte Herbstausrüstung 2025.",
"armorMystery202509Text": "Robe des Windgepeitschten Wanderers",
"armorMystery202509Notes": "Helle Seide schützt dich vor dem Wetter - sowohl heiß als auch kalt. Gewährt keinen Attributbonus. September 2025 Abonnentengegenstand.",
"armorArmoireRedWaistcoatText": "Rote Weste",
"armorArmoireSoftOrangeSuitText": "Weicher Orangener Anzug",
"armorArmoireBlacksmithsApronText": "Schmiedeschürze",
"armorArmoireBlacksmithsApronNotes": "Diese Schürze fühlt sich nicht so schwer an, wie sie aussieht, sobald du sie trägst. Sie wird dich vor Funken schützen und dir gleichzeitig ermöglichen, dich frei zu bewegen. Erhöht Ausdauer um <%= con %>. Verzauberter Schrank: Schmied Set (Gegenstand 2 von 3).",
"armorArmoireBlackPartyDressText": "Schwarzes Party Kleid",
"armorArmoireBlackPartyDressNotes": "Du bist stark, schlau, tüchtig und so modisch! Erhöht Stärke, Intelligenz und Ausdauer um jeweils <%= attrs %>. Verzauberter Schrank: Schwarzes Haarschleifen-Set (Gegenstand 2 von 2).",
"headSpecialFall2025WarriorText": "Sasquatch Maske",
"headSpecialFall2025WarriorNotes": "Rund und haarig - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Stärke um <%= str %>. Limitierte Herbstausrüstung 2025.",
"headSpecialFall2025RogueText": "Skelett Maske",
"headSpecialFall2025RogueNotes": "Blass und vermummt - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Wahrnehmung um <%= per %>. Limitierte Herbstausrüstung 2025.",
"headSpecialFall2025HealerText": "Kobold Maske",
"headSpecialFall2025MageText": "Maskierter Geist Maske",
"headSpecialFall2025MageNotes": "Ätherisch und glühend - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Wahrnehmung um <%= per %>. Limitierte Herbstausrüstung 2025.",
"armorArmoireRedWaistcoatNotes": "Sieh elegant und umwerfend aus, während du deine Aufgaben bewältigst. In der Westentasche ist etwas geheimes versteckt — was denkst du, könnte es sein? Erhöht Ausdauer und Stärke um jeweils <%= attrs %>. Verzauberter Schrank: Rote Weste Set (Gegenstand 2 von 2)",
"armorArmoireSoftOrangeSuitNotes": "Orange ist eine lebhafte Farbe. Zieh dies an, wenn du zu Bett gehst und in allen Abenteuern, denen du in deinen Träumen begegnest, wirst du sicher Erfolg haben. Erhöht Ausdauer und Stärke um jeweils <%= attrs %> . Verzauberter Schrank: Oranges Loungewear-Set (Gegenstand 2 von 3).",
"headSpecialFall2025HealerNotes": "Markant und gehörnt - diese Maske bedeckt deinen Kopf, während du alle deine wichtigen Aufgaben abdeckst. Erhöht Intelligenz um <%= int %>. Limitierte Herbstausrüstung 2025.",
"headArmoireRedNewsieHatText": "Rote Zeitungsjungenmütze",
"headArmoireRedNewsieHatNotes": "Extra! Extra! Lesen Sie alles darüber: Diese Mütze ist bequem, modisch und praktisch. Erhöht die Wahrnehmung und Intelligenz um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Rotes Weste-Set (Item 1 von 2)",
"headArmoireFloppyOrangeHatText": "Orangener Schlapphut",
"headArmoireFloppyOrangeHatNotes": "In diesen simplen Hut wurden zahlreiche Zauber eingearbeitet, die ihm eine auffällige orange Farbe verleihen. Erhöht alle Werte um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Orangenes Loungewear-Set (Item 1 von 3).",
"headArmoireBlackHairbowText": "Schwarze Haarschleife",
"headArmoireBlackHairbowNotes": "Werde stark, klug und herzhaft, während du diese wunderschöne schwarze Haarschleife trägst! Erhöht Stärke, Intelligenz und Konstitution um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Schwarzes Haarschleifen-Set (Item 1 von 2).",
"headArmoireBlacksmithsGogglesText": "Schmiedebrille",
"shieldSpecialFall2025RogueNotes": "Eine mächtige Waffe, mit der Sie Ihre To-Do's um die Hälfte reduzieren können. Erhöht Stärke um <%= str %>. Limitierte Ausgabe Herbst 2025 Ausrüstung.",
"shieldSpecialFall2025HealerText": "Koboldschild",
"shieldMystery202508Text": "Brillante Cyan-Klinge",
"shieldMystery202508Notes": "Wenn Sie schon eine rotierende Klinge cool fanden, probieren Sie doch mal zwei! Bietet keinen Vorteil. August 2025 Abonnentengegenstand.",
"shieldMystery202511Text": "Frostschild",
"shieldMystery202511Notes": "Dieser robuste Schild aus eisigem Gestein schützt dich vor schlechten Gewohnheiten, ohne deine Hände zu vereisen. Verleiht keinen Vorteil. November 2025 Abonnentengegenstand.",
"shieldArmoireSoftOrangePillowText": "Weiches orangenes Kissen",
"backMystery202510Text": "Gleitende Ghulflügel",
"backMystery202510Notes": "Fliege mit diesen riesigen Flügeln lautlos durch den heimgesuchten Himmel. Verleiht keinen Vorteil. Oktober 2025 Abonnentengegenstand.",
"bodyMystery202509Notes": "Dieser Schal schützt dein Gesicht vor Wind und sieht auch noch verdammt cool aus. Bietet keinen Vorteil. September 2025 Abonnentengegenstand.",
"eyewearMystery202510Text": "Gleitende Ghul-Augen",
"eyewearMystery202510Notes": "Diese gruseligen Augen leuchten wie der Erntemond. Verleiht keinen Vorteil. Oktober 2025 Abonnentengegenstand.",
"headArmoireBlacksmithsGogglesNotes": "Bei der Arbeit in einer Schmiede benötigen Sie einen bruchsicheren und hitzebeständigen Augenschutz. Erhöht die Wahrnehmung um <%= per %>. Verzauberter Schrank: Schmiedeset (Item 1 von 3).",
"shieldSpecialFall2025WarriorText": "Sasquatch Schild",
"shieldSpecialFall2025WarriorNotes": "Verschaffe dir etwas mehr Zeit zum Nachdenken und Planen, indem du dich vor deinen nächsten Tagesaufgaben abschirmst. Erhöht die Konstitution um <%= con %>. Limitierte Auflage Herbst 2025 Ausrüstung.",
"shieldSpecialFall2025HealerNotes": "Verschaffe dir etwas mehr Zeit, um Vorräte zu sammeln, indem du dich vor deinen Aufgaben abschirmst. Erhöht die Konstitution um <%= con %>. Limitierte Ausgabe Herbst 2025 Ausrüstung.",
"shieldArmoireSoftOrangePillowNotes": "Der vorbereitete Krieger packt für jede Expedition ein Kissen ein. Mach dich bereit, neue Verpflichtungen zu übernehmen ... sogar während du ein Nickerchen machst. Erhöht Intelligenz und Wahrnehmung um jeweils <%= attrs %>. Verzauberter Kleiderschrank: Orangenes Loungewear-Set (Gegenstand 3 von 3).",
"bodyMystery202509Text": "Schal des windgepeitschten Wanderers",
"armorSpecialFall2025RogueNotes": "Ein hartes und schmales Ziel in dieser saisonalen Rüstung ist am schwersten zu treffen. Erhöht die Wahrnehmung um <%= per %>. Limitierte Ausgabe Herbst 2025 Ausrüstung."
}

View File

@@ -241,5 +241,6 @@
"titleCustomizations": "Individualisierungen",
"targetUserNotExist": "Zielbenutzer: '<%= userName %>' existiert nicht.",
"newMessage": "Neue Nachricht",
"rememberToBeKind": "Bitte sei freundlich, respektvoll, und folge den <a href='/static/community-guidelines' target='_blank'>Community-Richtlinien</a>."
"rememberToBeKind": "Bitte sei freundlich, respektvoll, und folge den <a href='/static/community-guidelines' target='_blank'>Community-Richtlinien</a>.",
"gem": "Edelstein"
}

View File

@@ -365,12 +365,6 @@
"groupUseDefault": "Wähle eine Antwort",
"createGroup": "Erstelle eine Gruppe",
"groupUse": "Was beschreibt den Zweck Deiner Gruppe am Besten?*",
"groupParentChildren": "Eltern(teile), die Aufgaben für ihre Kinder erstellen",
"groupCouple": "Ein Paar, das sich Aufgaben teilt",
"groupFriends": "Freunde, die sich Aufgaben teilen",
"groupCoworkers": "Arbeitskollegen, die sich Aufgaben teilen",
"groupManager": "Ein Manager, der Aufgaben für seine Mitarbeiter erstellt",
"groupTeacher": "Ein Lehrer, der Aufgaben für seine Schüler oder Studierenden erstellt",
"nameStar": "Name*",
"descriptionOptional": "Beschreibung",
"descriptionOptionalText": "Füge eine Beschreibung hinzu",

View File

@@ -1,5 +1,5 @@
{
"stable": "Haus- und Reittiere",
"stable": "Haustiere und Reittiere",
"pets": "Haustiere",
"activePet": "Aktives Haustier",
"noActivePet": "Kein aktives Haustier",

View File

@@ -2,7 +2,7 @@
"settings": "Einstellungen",
"language": "Sprache",
"americanEnglishGovern": "Im Fall von Bedeutungsunterschieden gilt die englische Version.",
"helpWithTranslation": "Hast du Interesse, bei der Übersetzung von Habitica helfen? Toll! Dann besuche doch die <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">Aspiring Linguists Guild</a>!",
"helpWithTranslation": "Hast du Interesse, bei der Übersetzung von Habitica helfen? Toll! Dann besuche doch die <a href=\"https://translate.habitica.com\"> Habitica's Weblate Seite</a>!",
"stickyHeader": "Kopfzeile anheften",
"newTaskEdit": "Neue Aufgaben im Bearbeiten-Modus öffnen",
"reverseChatOrder": "Zeige die Chat-Nachrichten in umgekehrter Reihenfolge",

View File

@@ -3,5 +3,9 @@
"siteBlockers": "Site Blockers",
"newsroom": "Newsroom",
"adminBlockerTypeDescription": "<b>IP-Address</b> - Block access for a specific IP-Address\n\nClient - Block access for a client based on the \"x-client\" header.\n\nE-Mail - Blocks e-mails from being used for signup.",
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed."
"adminBlockerAreaDescription": "A blocker can either apply to the full site, completely blocking any access. Or it can apply to purchases, which still allows the site to be accessed.",
"groupAdmin": "Group Admin",
"groupSupportDescription": "Manage groups and their members. You can search for groups by ID, or load your own group by leaving the field blank.",
"groupData": "Group Data",
"groupPlanSubscription": "Group Plan Subscription"
}

View File

@@ -69,6 +69,7 @@
"awardWinners": "Award Winner",
"doYouWantedToDeleteChallenge": "Do you want to delete this Challenge?",
"deleteChallenge": "Delete Challenge",
"deleteChallengeRefundDescription": "If you delete this Challenge, you will be refunded the Gem prize and the Challenge tasks will remain on the participants' task boards.",
"challengeNamePlaceholder": "What is your Challenge name?",
"challengeSummary": "Summary",
"challengeSummaryPlaceholder": "Write a short description advertising your Challenge to other Habiticans. What is the main purpose of your Challenge and why should people join it? Try to include useful keywords in the description so that Habiticans can easily find it when they search!",

View File

@@ -1,195 +1,204 @@
{
"communityGuidelinesWarning": "Please keep in mind that your Display Name, profile photo, and blurb must comply with the <a href='https://habitica.com/static/community-guidelines' target='_blank'>Community Guidelines</a> (e.g. no profanity, no adult topics, no insults, etc). If you have any questions about whether or not something is appropriate, feel free to email <%= hrefBlankCommunityManagerEmail %>!",
"profile": "Profile",
"avatar": "Customize Avatar",
"editAvatar": "Customize Avatar",
"noDescription": "This Habitican hasn't added a description.",
"noPhoto": "This Habitican hasn't added a photo.",
"other": "Other",
"fullName": "Full Name",
"displayName": "Display name",
"changeDisplayName": "Change Display Name",
"newDisplayName": "New Display Name",
"displayBlurbPlaceholder": "Please introduce yourself",
"photoUrl": "Photo Url",
"imageUrl": "Image Url",
"inventory": "Inventory",
"social": "Social",
"lvl": "Lvl",
"buffed": "Buffed",
"bodyBody": "Body",
"size": "Size",
"locked": "locked",
"shirts": "Shirts",
"shirt": "Shirt",
"specialShirts": "Special Shirts",
"skin": "Skin",
"skins": "Skins",
"color": "Color",
"hair": "Hair",
"bangs": "Bangs",
"glasses": "Glasses",
"hairSet1": "Hairstyle Set 1",
"hairSet2": "Hairstyle Set 2",
"hairSet3": "Hairstyle Set 3",
"beard": "Beard",
"mustache": "Mustache",
"titleFacialHair": "Facial Hair",
"titleHaircolor": "Hair Colors",
"titleHairbase": "Hair Styles",
"flower": "Flower",
"accent": "Accent",
"headband": "Headband",
"wheelchair": "Wheelchair",
"extra": "Extra",
"rainbowSkins": "Rainbow Skins",
"pastelSkins": "Pastel Skins",
"spookySkins": "Spooky Skins",
"supernaturalSkins": "Supernatural Skins",
"splashySkins": "Splashy Skins",
"winterySkins": "Wintery Skins",
"rainbowColors": "Rainbow Colors",
"shimmerColors": "Shimmer Colors",
"hauntedColors": "Haunted Colors",
"winteryColors": "Wintery Colors",
"equipment": "Equipment",
"equipmentBonus": "Equipment",
"classEquipBonus": "Class Bonus",
"battleGear": "Battle Gear",
"gear": "Gear",
"autoEquipBattleGear": "Auto-equip new gear",
"costume": "Costume",
"useCostume": "Use Costume",
"costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the Stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.",
"autoEquipPopoverText": "Select this option to automatically equip gear as soon as you purchase it.",
"costumeDisabled": "You have disabled your costume.",
"gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class! You have attained the following complete sets:",
"gearAchievementNotification": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class!",
"moreGearAchievements": "To attain more Ultimate Gear badges, change classes on <a href='/user/settings/site' target='_blank'>the Settings &gt; Site page</a> and buy your new class's gear!",
"armoireUnlocked": "For more equipment, check out the <strong>Enchanted Armoire!</strong> Click on the Enchanted Armoire Reward for a random chance at special Equipment! It may also give you random XP or food items.",
"ultimGearName": "Ultimate Gear - <%= ultClass %>",
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the <%= ultClass %> class.",
"level": "Level",
"levelUp": "Level Up!",
"gainedLevel": "You gained a level!",
"leveledUp": "By accomplishing your real-life goals, you've grown to <strong>Level <%= level %>!</strong>",
"huzzah": "Huzzah!",
"mana": "Mana",
"hp": "HP",
"mp": "MP",
"xp": "XP",
"health": "Health",
"allocateStr": "Points allocated to Strength:",
"allocateStrPop": "Add a Point to Strength",
"allocateCon": "Points allocated to Constitution:",
"allocateConPop": "Add a Point to Constitution",
"allocatePer": "Points allocated to Perception:",
"allocatePerPop": "Add a Point to Perception",
"allocateInt": "Points allocated to Intelligence:",
"allocateIntPop": "Add a Point to Intelligence",
"noMoreAllocate": "Now that you've hit level 100, you won't gain any more Stat Points. You can continue leveling up, or start a new adventure at level 1 by using the <a href='/shops/market'>Orb of Rebirth</a>!",
"stats": "Stats",
"strength": "Strength",
"strText": "Strength increases the chance of random \"critical hits\" and the Gold, Experience, and drop chance boost from them. It also helps deal damage to boss monsters.",
"constitution": "Constitution",
"conText": "Constitution reduces the damage you take from negative Habits and missed Dailies.",
"perception": "Perception",
"perText": "Perception increases how much Gold you earn, and once you've unlocked the Market, increases the chance of finding items when scoring tasks.",
"intelligence": "Intelligence",
"intText": "Intelligence increases how much Experience you earn, and once you've unlocked Classes, determines your maximum Mana available for class abilities.",
"levelBonus": "Level Bonus",
"allocatedPoints": "Allocated Points",
"allocated": "Allocated",
"buffs": "Buffs",
"characterBuild": "Character Build",
"class": "Class",
"experience": "Experience",
"warrior": "Warrior",
"healer": "Healer",
"rogue": "Rogue",
"mage": "Mage",
"wizard": "Mage",
"mystery": "Mystery",
"changeClass": "Change Class, Refund Stat Points",
"lvl10ChangeClass": "To change class you must be at least level 10.",
"changeClassConfirmCost": "Are you sure you want to change your class for 3 Gems?",
"invalidClass": "Invalid class. Please specify 'warrior', 'rogue', 'wizard', or 'healer'.",
"levelPopover": "Each level earns you one Point to assign to a Stat of your choice. You can do so manually, or let the game decide for you using one of the Automatic Allocation options.",
"unallocated": "Unallocated Stat Points",
"autoAllocation": "Automatic Allocation",
"autoAllocationPop": "Places Points into Stats according to your preferences, when you level up.",
"evenAllocation": "Distribute Stat Points evenly",
"evenAllocationPop": "Assigns the same number of Points to each Stat.",
"classAllocation": "Distribute Points based on Class",
"classAllocationPop": "Assigns more Points to the Stats important to your Class.",
"taskAllocation": "Distribute Points based on task activity",
"taskAllocationPop": "Assigns Points based on the Strength, Intelligence, Constitution, and Perception categories associated with the tasks you complete.",
"distributePoints": "Distribute Unallocated Points",
"distributePointsPop": "Assigns all unallocated Stat Points according to the selected allocation scheme.",
"warriorText": "Warriors score more and better \"critical hits\", which randomly give bonus Gold, Experience, and drop chance for scoring a task. They also deal heavy damage to boss monsters. Play a Warrior if you find motivation from unpredictable jackpot-style rewards, or want to dish out the hurt in boss Quests!",
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
"mageText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
"rogueText": "Rogues love to accumulate wealth, gaining more Gold than anyone else, and are adept at finding random items. Their iconic Stealth ability lets them duck the consequences of missed Dailies. Play a Rogue if you find strong motivation from Rewards and Achievements, striving for loot and badges!",
"healerText": "Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you!",
"optOutOfClasses": "Opt Out",
"chooseClass": "Choose your Class",
"chooseClassLearnMarkdown": "[Learn more about Habitica's class system](/static/faq#what-classes)",
"optOutOfClassesText": "Not ready to choose? There's no rush! If you opt out, you can read about each Class in <a href='/static/faq#what-classes' target='_blank'>our FAQ</a> and visit Settings to enable the Class System when you're ready.",
"selectClass": "Select <%= heroClass %>",
"select": "Select",
"stealth": "Stealth",
"stealthNewDay": "When a new day begins, you will avoid damage from this many missed Dailies.",
"streaksFrozen": "Streaks Frozen",
"streaksFrozenText": "Streaks on missed Dailies will not reset at the end of the day.",
"purchaseFor": "Purchase for <%= cost %> Gems?",
"purchaseForGold": "Purchase for <%= cost %> Gold?",
"purchaseForHourglasses": "Purchase for <%= cost %> Hourglasses?",
"purchasePetItemConfirm": "This purchase would exceed the number of items you need to hatch all possible <%= itemText %> pets. Are you sure?",
"notEnoughMana": "Not enough mana.",
"notEnoughGold": "Not enough gold.",
"invalidTarget": "You can't cast a skill on that.",
"youCast": "You cast <%= spell %>.",
"youCastTarget": "You cast <%= spell %> on <%= target %>.",
"youCastParty": "You cast <%= spell %> for the party.",
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
"chatCastSpellPartyTimes": "<%= username %> casts <%= spell %> for the party <%= times %> times.",
"chatCastSpellUserTimes": "<%= username %> casts <%= spell %> on <%= target %> <%= times %> times.",
"critBonus": "Critical Hit! Bonus: ",
"gainedGold": "You gained some Gold",
"gainedMana": "You gained some Mana",
"gainedHealth": "You gained some Health",
"gainedExperience": "You gained some Experience",
"lostGold": "You spent some Gold",
"lostMana": "You used some Mana",
"lostHealth": "You lost some Health",
"lostExperience": "You lost some Experience",
"equip": "Equip",
"unequip": "Unequip",
"animalSkins": "Animal Skins",
"str": "STR",
"con": "CON",
"per": "PER",
"int": "INT",
"notEnoughAttrPoints": "You don't have enough Stat Points.",
"classNotSelected": "You must select Class before you can assign Stat Points.",
"style": "Style",
"facialhair": "Facial",
"photo": "Photo",
"info": "Info",
"joined": "Joined",
"totalLogins": "Total Log Ins",
"latestCheckin": "Latest Log In",
"nextReward": "Next Log In Reward",
"editProfile": "Edit Profile",
"challengesWon": "Challenges Won",
"questsCompleted": "Quests Completed",
"headAccess": "Head Access.",
"backAccess": "Back Access.",
"bodyAccess": "Body Access.",
"mainHand": "Main-Hand",
"offHand": "Off-Hand",
"statPoints": "Stat Points",
"pts": "pts",
"customizations": "Customizations"
}
{
"communityGuidelinesWarning": "Please keep in mind that your Display Name, profile photo, and blurb must comply with the <a href='https://habitica.com/static/community-guidelines' target='_blank'>Community Guidelines</a> (e.g. no profanity, no adult topics, no insults, etc). If you have any questions about whether or not something is appropriate, feel free to email <%= hrefBlankCommunityManagerEmail %>!",
"profile": "Profile",
"avatar": "Customize Avatar",
"editAvatar": "Customize Avatar",
"noDescription": "This Habitican hasn't added a description.",
"noPhoto": "This Habitican hasn't added a photo.",
"other": "Other",
"fullName": "Full Name",
"displayName": "Display name",
"changeDisplayName": "Change Display Name",
"newDisplayName": "New Display Name",
"displayBlurbPlaceholder": "Please introduce yourself",
"photoUrl": "Photo Url",
"imageUrl": "Image Url",
"inventory": "Inventory",
"social": "Social",
"lvl": "Lvl",
"buffed": "Buffed",
"bodyBody": "Body",
"size": "Size",
"locked": "locked",
"shirts": "Shirts",
"shirt": "Shirt",
"specialShirts": "Special Shirts",
"skin": "Skin",
"skins": "Skins",
"color": "Color",
"hair": "Hair",
"bangs": "Bangs",
"glasses": "Glasses",
"hairSet1": "Hairstyle Set 1",
"hairSet2": "Hairstyle Set 2",
"hairSet3": "Hairstyle Set 3",
"beard": "Beard",
"mustache": "Mustache",
"titleFacialHair": "Facial Hair",
"titleHaircolor": "Hair Colors",
"titleHairbase": "Hair Styles",
"flower": "Flower",
"accent": "Accent",
"headband": "Headband",
"wheelchair": "Wheelchair",
"extra": "Extra",
"rainbowSkins": "Rainbow Skins",
"pastelSkins": "Pastel Skins",
"spookySkins": "Spooky Skins",
"supernaturalSkins": "Supernatural Skins",
"splashySkins": "Splashy Skins",
"winterySkins": "Wintery Skins",
"rainbowColors": "Rainbow Colors",
"shimmerColors": "Shimmer Colors",
"hauntedColors": "Haunted Colors",
"winteryColors": "Wintery Colors",
"equipment": "Equipment",
"equipmentBonus": "Equipment",
"classEquipBonus": "Class Bonus",
"battleGear": "Battle Gear",
"gear": "Gear",
"autoEquipBattleGear": "Auto-equip new gear",
"costume": "Costume",
"useCostume": "Use Costume",
"costumePopoverText": "Select \"Use Costume\" to equip items to your avatar without affecting the Stats from your Battle Gear! This means that you can dress up your avatar in whatever outfit you like while still having your best Battle Gear equipped.",
"autoEquipPopoverText": "Select this option to automatically equip gear as soon as you purchase it.",
"costumeDisabled": "You have disabled your costume.",
"gearAchievement": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class! You have attained the following complete sets:",
"gearAchievementNotification": "You have earned the \"Ultimate Gear\" Achievement for upgrading to the maximum gear set for a class!",
"moreGearAchievements": "To attain more Ultimate Gear badges, change classes on <a href='/user/settings/site' target='_blank'>the Settings &gt; Site page</a> and buy your new class's gear!",
"armoireUnlocked": "For more equipment, check out the <strong>Enchanted Armoire!</strong> Click on the Enchanted Armoire Reward for a random chance at special Equipment! It may also give you random XP or food items.",
"ultimGearName": "Ultimate Gear - <%= ultClass %>",
"ultimGearText": "Has upgraded to the maximum weapon and armor set for the <%= ultClass %> class.",
"level": "Level",
"levelUp": "Level Up!",
"gainedLevel": "You gained a level!",
"leveledUp": "By accomplishing your real-life goals, you've grown to <strong>Level <%= level %>!</strong>",
"huzzah": "Huzzah!",
"mana": "Mana",
"hp": "HP",
"mp": "MP",
"xp": "XP",
"health": "Health",
"allocateStr": "Points allocated to Strength:",
"allocateStrPop": "Add a Point to Strength",
"allocateCon": "Points allocated to Constitution:",
"allocateConPop": "Add a Point to Constitution",
"allocatePer": "Points allocated to Perception:",
"allocatePerPop": "Add a Point to Perception",
"allocateInt": "Points allocated to Intelligence:",
"allocateIntPop": "Add a Point to Intelligence",
"noMoreAllocate": "Now that you've hit level 100, you won't gain any more Stat Points. You can continue leveling up, or start a new adventure at level 1 by using the <a href='/shops/market'>Orb of Rebirth</a>.",
"stats": "Stats",
"strength": "Strength",
"strText": "Strength increases the chance of random \"critical hits\" and the Gold, Experience, and drop chance boost from them. It also helps deal damage to boss monsters.",
"strTaskText": "Increases critical hit chance and damage when scoring tasks. Also increases damage dealt to bosses.",
"constitution": "Constitution",
"conText": "Constitution reduces the damage you take from negative Habits and missed Dailies.",
"conTaskText": "Reduces damage taken from missed Dailies and negative Habits. Does not reduce damage from bosses.",
"perception": "Perception",
"perText": "Perception increases how much Gold you earn, and once you've unlocked the Market, increases the chance of finding items when scoring tasks.",
"perTaskText": "Increases item drop chance, daily item drop cap, task streak bonuses, and Gold earned when completing tasks.",
"intelligence": "Intelligence",
"intText": "Intelligence increases how much Experience you earn, and once you've unlocked Classes, determines your maximum Mana available for class abilities.",
"intTaskText": "Increases Exp earned from tasks. Also increases your mana cap and mana regeneration rate.",
"levelBonus": "Level Bonus",
"allocatedPoints": "Allocated Points",
"allocated": "Allocated",
"buffs": "Buffs",
"characterBuild": "Character Build",
"class": "Class",
"experience": "Experience",
"warrior": "Warrior",
"healer": "Healer",
"rogue": "Rogue",
"mage": "Mage",
"wizard": "Mage",
"mystery": "Mystery",
"changeClass": "Change Class, Refund Stat Points",
"lvl10ChangeClass": "To change class you must be at least level 10.",
"changeClassConfirmCost": "Are you sure you want to change your class for 3 Gems?",
"invalidClass": "Invalid class. Please specify 'warrior', 'rogue', 'wizard', or 'healer'.",
"levelPopover": "Each level earns you one Point to assign to a Stat of your choice. You can do so manually, or let the game decide for you using one of the Automatic Allocation options.",
"unallocated": "Unallocated Stat Points",
"autoAllocation": "Automatic Allocation",
"autoAllocate": "Auto Allocate",
"autoAllocationPop": "Places Points into Stats according to your preferences, when you level up.",
"evenAllocation": "Distribute Evenly",
"evenAllocationPop": "Assigns the same number of points to each attribute.",
"classAllocation": "Distribute based on Class",
"classAllocationPop": "Assigns more points to the attributes important to your Class.",
"taskAllocation": "Distribute Points based on task activity",
"taskAllocationPop": "Assigns Points based on the Strength, Intelligence, Constitution, and Perception categories associated with the tasks you complete.",
"distributePoints": "Distribute Unallocated Points",
"distributePointsPop": "Assigns all unallocated Stat Points according to the selected allocation scheme.",
"warriorText": "Warriors score more and better \"critical hits\", which randomly give bonus Gold, Experience, and drop chance for scoring a task. They also deal heavy damage to boss monsters. Play a Warrior if you find motivation from unpredictable jackpot-style rewards, or want to dish out the hurt in boss Quests!",
"wizardText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
"mageText": "Mages learn swiftly, gaining Experience and Levels faster than other classes. They also get a great deal of Mana for using special abilities. Play a Mage if you enjoy the tactical game aspects of Habitica, or if you are strongly motivated by leveling up and unlocking advanced features!",
"rogueText": "Rogues love to accumulate wealth, gaining more Gold than anyone else, and are adept at finding random items. Their iconic Stealth ability lets them duck the consequences of missed Dailies. Play a Rogue if you find strong motivation from Rewards and Achievements, striving for loot and badges!",
"healerText": "Healers stand impervious against harm, and extend that protection to others. Missed Dailies and bad Habits don't faze them much, and they have ways to recover Health from failure. Play a Healer if you enjoy assisting others in your Party, or if the idea of cheating Death through hard work inspires you!",
"optOutOfClasses": "Opt Out",
"chooseClass": "Choose your Class",
"chooseClassLearnMarkdown": "[Learn more about Habitica's class system](/static/faq#what-classes)",
"optOutOfClassesText": "Not ready to choose? There's no rush! If you opt out, you can read about each Class in <a href='/static/faq#what-classes' target='_blank'>our FAQ</a> and visit Settings to enable the Class System when you're ready.",
"selectClass": "Select <%= heroClass %>",
"select": "Select",
"stealth": "Stealth",
"stealthNewDay": "When a new day begins, you will avoid damage from this many missed Dailies.",
"streaksFrozen": "Streaks Frozen",
"streaksFrozenText": "Streaks on missed Dailies will not reset at the end of the day.",
"purchaseFor": "Purchase for <%= cost %> Gems?",
"purchaseForGold": "Purchase for <%= cost %> Gold?",
"purchaseForHourglasses": "Purchase for <%= cost %> Hourglasses?",
"purchasePetItemConfirm": "This purchase would exceed the number of items you need to hatch all possible <%= itemText %> pets. Are you sure?",
"notEnoughMana": "Not enough mana.",
"notEnoughGold": "Not enough gold.",
"invalidTarget": "You can't cast a skill on that.",
"youCast": "You cast <%= spell %>.",
"youCastTarget": "You cast <%= spell %> on <%= target %>.",
"youCastParty": "You cast <%= spell %> for the party.",
"chatCastSpellParty": "<%= username %> casts <%= spell %> for the party.",
"chatCastSpellUser": "<%= username %> casts <%= spell %> on <%= target %>.",
"chatCastSpellPartyTimes": "<%= username %> casts <%= spell %> for the party <%= times %> times.",
"chatCastSpellUserTimes": "<%= username %> casts <%= spell %> on <%= target %> <%= times %> times.",
"critBonus": "Critical Hit! Bonus: ",
"gainedGold": "You gained some Gold",
"gainedMana": "You gained some Mana",
"gainedHealth": "You gained some Health",
"gainedExperience": "You gained some Experience",
"lostGold": "You spent some Gold",
"lostMana": "You used some Mana",
"lostHealth": "You lost some Health",
"lostExperience": "You lost some Experience",
"equip": "Equip",
"unequip": "Unequip",
"animalSkins": "Animal Skins",
"str": "STR",
"con": "CON",
"per": "PER",
"int": "INT",
"notEnoughAttrPoints": "You don't have enough Stat Points.",
"classNotSelected": "You must select Class before you can assign Stat Points.",
"style": "Style",
"facialhair": "Facial",
"photo": "Photo",
"info": "Info",
"joined": "Joined",
"totalLogins": "Total Log Ins",
"latestCheckin": "Latest Log In",
"nextReward": "Next Log In Reward",
"editProfile": "Edit Profile",
"challengesWon": "Challenges Won",
"questsCompleted": "Quests Completed",
"headAccess": "Head Access.",
"backAccess": "Back Access.",
"bodyAccess": "Body Access.",
"mainHand": "Main-Hand",
"offHand": "Off-Hand",
"statPoints": "Stat Points",
"pointsAvailable": "Points Available",
"allocationMethod": "Allocation Method",
"statAllocationInfo": "Each level earns you one point to assign to a Stat of your choice. You can do so manually, or let the game decide for you using one of the Automatic Allocation options.",
"pts": "PTS",
"customizations": "Customizations",
"assignedStat": "Assigned Stat"
}

View File

@@ -133,7 +133,7 @@
"passwordReset": "If we have your email or username on file, instructions for setting a new password have been sent to your email.",
"invalidLoginCredentialsLong": "Your email, username, or password are incorrect. Please try again or use \"Forgot Password.\"",
"invalidCredentials": "There is no account that uses those credentials.",
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the Community Guidelines (https://habitica.com/static/community-guidelines) or Terms of Service (https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please include your @Username in the email.",
"accountSuspended": "Your account @<%= username %> has been blocked. For additional information, or to request an appeal, email admin@habitica.com with your Habitica username or User ID.",
"accountSuspendedTitle": "Account has been suspended",
"unsupportedNetwork": "This network is not currently supported.",
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",

View File

@@ -51,6 +51,7 @@
"notEnoughGems": "Not enough Gems",
"alreadyHave": "Whoops! You already have this item. No need to buy it again!",
"delete": "Delete",
"gem": "Gem",
"gems": "Gems",
"needMoreGems": "Need More Gems?",
"needMoreGemsInfo": "Purchase Gems now, or become a subscriber to buy Gems with Gold, get monthly mystery items, enjoy increased drop caps and more!",

View File

@@ -116,7 +116,7 @@
"needsTextPlaceholder": "Type your message here.",
"messageCopiedToClipboard": "Message copied to clipboard.",
"leaderOnlyChallenges": "Only group leader can create challenges",
"sendGift": "Send a Gift",
"sendGift": "Send Gift",
"selectGift": "Select Gift",
"selectSubscription": "Select Subscription",
"sendGiftToWhom": "Who would you like to send a gift to?",
@@ -358,12 +358,10 @@
"createGroup": "Create a Group",
"groupUse": "Which best describes the use of your Group?*",
"groupUseDefault": "Choose an answer",
"groupParentChildren": "Parent(s) setting up tasks for children",
"groupCouple": "Couple sharing tasks",
"groupFriends": "Friends sharing tasks",
"groupCoworkers": "Coworkers sharing tasks",
"groupManager": "Manager setting up tasks for employees",
"groupTeacher": "Teacher setting up tasks for students",
"groupParentChildren": "Using with my household",
"groupFriends": "Using with friends",
"groupManager": "Using for work",
"groupTeacher": "Using for education",
"gettingStarted": "Getting Started",
"nameStar": "Name*",
"nameStarText": "Add a title",

View File

@@ -104,7 +104,6 @@
"success": "Success!",
"classGear": "Class Gear",
"classGearText": "Congratulations on choosing a class! I've added your new basic weapon to your inventory. Take a look below to equip it!",
"autoAllocate": "Auto Allocate",
"spells": "Skills",
"skillsTitle": "<%= classStr %> Skills",
"toDo": "To Do",

View File

@@ -271,5 +271,7 @@
"performanceAnalytics": "Performance and Analytics",
"usedForSupport": "These are used to improve the user experience, performance, and services of our website and apps. This data is used by our support team when handling requests and bug reports.",
"savePreferences": "Save Preferences",
"habiticaPrivacyPolicy": "Habitica's Privacy Policy"
"habiticaPrivacyPolicy": "Habitica's Privacy Policy",
"gpcWarning": "<a href='<%= url %>' target='_blank'>GPC</a> is on. Turning on tracking below will override this and send data to our analytics partners.",
"gpcPlusAnalytics": "<a href='<%= url %>' target='_blank'>GPC</a> is on. You have opted in to tracking and sending data to our analytics partners."
}

View File

@@ -901,13 +901,25 @@
"backgrounds0420205": "SET 131: Released April 2025",
"backgroundGardenWithFlowerBedsText": "Garden with Flower Beds",
"backgroundGardenWithFlowerBedsNotes": "Enjoy the blooms of spring in a Garden with Flower Beds.",
"backgrounds062025": " 133由2025 年 6 月发布",
"backgroundSummerSeashoreText": "夏日海滨",
"backgroundSummerSeashoreNotes": "在夏日海滨乘风破浪.",
"backgrounds052025": " 132于2025 年 5 月发布",
"backgroundTrailThroughAForestText": "穿越森林的小径",
"backgroundTrailThroughAForestNotes": "沿着穿过森林的小径漫步。",
"backgrounds062025": "SET 133: Released June 2025",
"backgroundSummerSeashoreText": "Summer Seashore",
"backgroundSummerSeashoreNotes": "Catch a wave at a Summer Seashore.",
"backgrounds052025": "SET 132: Released May 2025",
"backgroundTrailThroughAForestText": "Trail Through a Forest",
"backgroundTrailThroughAForestNotes": "Wander down a Trail Through a Forest.",
"backgrounds072025": "SET 134: Released July 2025",
"backgroundSirensLairText": "Siren's Lair",
"backgroundSirensLairNotes": "Dare to dive into a Sirens Lair."
"backgroundSirensLairNotes": "Dare to dive into a Sirens Lair.",
"backgrounds082025": "SET 135: Released August 2025",
"backgroundSunnyStreetWithShopsText": "Sunny Street with Shops",
"backgroundSunnyStreetWithShopsNotes": "Enjoy the sights and sounds of a Sunny Street with Shops.",
"backgrounds092025": "SET 136: Released September 2025",
"backgroundAutumnSwampText": "Autumn Swamp",
"backgroundAutumnSwampNotes": "Take in the haunting vibes of an Autumn Swamp.",
"backgrounds102025": "SET 137: Released October 2025",
"backgroundInsideForestWitchsCottageText": "Forest Witch's Cottage",
"backgroundInsideForestWitchsCottageNotes": "Weave spells inside a Forest Witch's Cottage.",
"backgrounds112025": "SET 138: Released November 2025",
"backgroundCastleKeepWithBannersText": "Castle Hall with Banners",
"backgroundCastleKeepWithBannersNotes": "Sing tales of heroic deeds in a Castle Hall with Banners."
}

View File

@@ -357,8 +357,6 @@
"editParty": "Edit Party",
"leaveGuild": "Leave Group",
"groupUseDefault": "Choose an answer",
"groupParentChildren": "Parent(s) setting up tasks for children",
"groupCouple": "Couple sharing tasks",
"viewStatus": "Status",
"chatTemporarilyUnavailable": "Chat is temporarily unavailable. Please try again later.",
"newGroupsBullet01": "Interact with tasks directly from the shared task board",
@@ -375,10 +373,6 @@
"viewDetails": "View Details",
"invitedToThisQuest": "You were invited to this Quest!",
"createGroup": "Create a Group",
"groupFriends": "Friends sharing tasks",
"groupCoworkers": "Coworkers sharing tasks",
"groupManager": "Manager setting up tasks for employees",
"groupTeacher": "Teacher setting up tasks for students",
"nameStar": "Name*",
"newGroupsBullet05": "Shared tasks will degrade in color if left incomplete to help track progress",
"newGroupsBullet06": "The task status view allows you to quickly see which assignee has completed a task",

View File

@@ -108,5 +108,6 @@
"resetFlags": "Reiniciar marcas",
"cannotClose": "Este Desafío no puede cerrarse porque uno o más jugadores lo han marcado como inapropiado. Un miembro del Personal te contactará pronto con instrucciones. Si han pasado más de 48 horas y no has recibido noticias de ellos, envía un correo a admin@habitica.com para solicitar ayuda.",
"cannotMakeChallenge": "No puedes crear un Desafío público porque tu cuenta no tiene privilegios de chat en este momento. Envía un mensaje a admin@habitica.com para obtener más información.",
"messageChallengeFlagOfficial": "Los Desafíos Oficiales no se pueden notificar."
"messageChallengeFlagOfficial": "Los Desafíos Oficiales no se pueden notificar.",
"deleteChallengeRefundDescription": "Si cancelas este Desafío, recuperarás las Gemas de la recompensa y las tareas del Desafío permanecerán en los listados de tareas de los participantes."
}

View File

@@ -124,7 +124,7 @@
"passwordReset": "Si tenemos constancia de tu correo electrónico o nombre de usuario, te hemos enviado un mensaje con las instrucciones a seguir para establecer una nueva contraseña.",
"invalidLoginCredentialsLong": "Tu correo electrónico o contraseña son incorrectos. Por favor intenta de nuevo o pulsa sobre \"¿Has olvidado la contraseña?\"",
"invalidCredentials": "No hay ninguna cuenta con esas credenciales.",
"accountSuspended": "Esta cuenta, con ID de usuario \"<%= userId %>\", ha sido bloqueada por incumplir las [Normas de la Comunidad](https://habitica.com/static/community-guidelines) o los [Términos de Servicio](https://habitica.com/static/terms). Para más detalles o solicitar su desbloqueo, por favor, envía un correo electrónico a nuestro Community Manager en <%= communityManagerEmail %> o pídele a tu padre o tutor que lo haga. Por favor, incluye tu @NombreDeUsuario en el mismo.",
"accountSuspended": "Tu cuenta @<%= username %> ha sido bloqueada. Para más información o para iniciar una apelación, por favor envía un email a admin@habitica.com indicando tu nombre de usuario en Habitica o tu ID de Usuario.",
"accountSuspendedTitle": "Esta cuenta ha sido suspendida",
"unsupportedNetwork": "La red no está en servicio.",
"cantDetachSocial": "La cuenta carece de otro método de autenticación; no se puede separar de este método de autenticación.",

View File

@@ -3386,10 +3386,10 @@
"weaponSpecialFall2025WarriorNotes": "Una poderosa arma capaz de trazar una senda segura a través de los peligros del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025RogueNotes": "Una poderosa arma capaz de trazar una senda segura a través de los peligros del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025RogueText": "Espada Esqueleto",
"weaponSpecialFall2025MageNotes": "Una poderosa arma capaz de trazar una senda segura a través de los terrores del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025MageNotes": "Una poderosa arma capaz de trazar una senda segura a través de los terrores del Bosque Negro. Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025WarriorText": "Hacha de Bigfoot",
"weaponSpecialFall2025HealerText": "Hacha Kobold",
"weaponSpecialFall2025HealerNotes": "Una poderosa arma capaz de trazar una senda segura a través de los obstáculos del Bosque Negro. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025HealerNotes": "Una poderosa arma capaz de trazar una senda segura a través de los obstáculos del Bosque Negro. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada Otoño 2025.",
"weaponSpecialFall2025MageText": "Hacha de Fantasma Enmascarado",
"weaponMystery202511Text": "Espada Escarcha",
"weaponMystery202511Notes": "El halo helado de esta espada te permitirá realizar con rapidez incluso las tareas rojas más oscuras. No otorga ningún beneficio. Artículo de Suscriptor Noviembre 2025.",

View File

@@ -241,5 +241,6 @@
"playerReportModalBody": "Solo deberías informar de un jugador que de alguna forma quebrantara las <%= firstLinkStart %>Normas de la Comunidad<%= linkEnd %> y/o <%= secondLinkStart %>los Terminos de Servicio<%= linkEnd %>. Enviar un informe falso es una clara violación de la Normas de la Comunidad de Habitica.",
"newMessage": "Nuevo Mensaje",
"targetUserNotExist": "Usuario objetivo: '<%= userName %>' no existe.",
"rememberToBeKind": "Por favor recuerda ser bondadoso, respetuoso y seguir las <a href='/static/community-guidelines' target='_blank'>Normas de la Comunidad</a>."
"rememberToBeKind": "Por favor recuerda ser bondadoso, respetuoso y seguir las <a href='/static/community-guidelines' target='_blank'>Normas de la Comunidad</a>.",
"gem": "Gema"
}

View File

@@ -96,7 +96,7 @@
"optional": "Opcional",
"needsTextPlaceholder": "Escribe tu mensaje aquí.",
"leaderOnlyChallenges": "Sólo el Líder del Grupo puede crear desafíos",
"sendGift": "Enviar un regalo",
"sendGift": "Enviar regalo",
"inviteFriends": "Invitar Amigos",
"inviteByEmail": "Invita por correo electrónico",
"inviteMembersHowTo": "Invita gente a través de un correo electrónico válido o el ID de usuario de 36 dígitos. Si el correo electrónico no ha sido registrado aún, le invitaremos a unirse a Habitica.",
@@ -373,10 +373,6 @@
"currentlyLookingForParty": "¡Estás buscando un Equipo!",
"partyFinderDescription": "¿Quieres a unirte a un Equipo pero no conoces a otros jugadores? Haz saber a los líderes de Equipo que esperas una invitación.",
"createGroup": "Crear un Grupo",
"groupFriends": "Amigos compartiendo tareas",
"groupCoworkers": "Compañeros de trabajo compartiendo tareas",
"groupManager": "Gerente organizando tareas para empleados",
"groupTeacher": "Profesor organizando tareas para estudiantes",
"nameStar": "Nombre*",
"nameStarText": "Añade un título",
"chatTemporarilyUnavailable": "El chat no está disponible en este momento. Inténtalo de nuevo más tarde.",
@@ -408,8 +404,6 @@
"assignTo": "Asignar a",
"lookForParty": "Buscar un Equipo",
"groupUse": "¿Cuál de las siguientes opciones describe mejor a tu Grupo?*",
"groupParentChildren": "Padres organizando tareas para niños",
"groupCouple": "Pareja compartiendo tareas",
"descriptionOptional": "Descripción",
"descriptionOptionalText": "Añade una descripción",
"nextPaymentMethod": "Siguiente: Pago",

View File

@@ -271,5 +271,7 @@
"privacySettingsOverview": "Habitica usa las cookies para analizar el rendimiento, gestionar solicitudes de soporte y brindarle la mejor experiencia de juego posible. Para eso, necesitamos pedirte los siguientes permisos. Puedes cambiar tus preferencias en cualquier momento desde los ajustes de tu cuenta.",
"usedForSupport": "Estás se usan para mejorar la experiencia del usuario, el rendimiento, y los servicios de nuestra página web y nuestras aplicaciones. Estos datos se usan para ayudar a nuestro equipo de soporte cuando tienen que gestionar tanto solicitudes como informes de error.",
"privacyOverview": "En el mundo en el que vivimos, parece que todas las compañías buscan obtener beneficios de los datos que recopilan acerca de nosotros. Esto nos dificulta la búsqueda de una aplicación que nos ayude a mejorar nuestros hábitos y estilo de vida. Habitica usa cookies que almacenan nuestros datos solo para analizar el rendimiento, gestionar solicitudes de soporte y brindarnos la mejor experiencia de juego posible. Aún así puedes cambiar tus preferencias en cualquier momento desde los ajustes de tu cuenta.",
"acceptAllCookies": "Aceptar Todas las Cookies"
"acceptAllCookies": "Aceptar Todas las Cookies",
"gpcWarning": "<a href='<%= url %>' target='_blank'>GPC</a>está activado. Al activar el seguimiento de abajo, se anulará esto y se enviarán datos a nuestros socios analíticos.",
"gpcPlusAnalytics": "<a href='<%= url %>' target='_blank'>GPC</a>está activado. Ha aceptado el seguimiento y envío de datos a nuestros socios analíticos."
}

View File

@@ -108,5 +108,6 @@
"flaggedNotHidden": "Desafío marcado una vez, No esta oculto",
"whyReportingChallenge": "Por qué estas reportando este Desafío?",
"abuseFlagModalBodyChallenge": "Deberías solo reportar un Desafío que viola las <%= firstLinkStart %>Directrices de la Comunidad<%= linkEnd %> y/o los <%= secondLinkStart %>Términos de Servicio<%= linkEnd %>. Envíar un reporte falso es una violación a las Directrices de la Comunidad.",
"resetFlags": "Reiniciar marcadores"
"resetFlags": "Reiniciar marcadores",
"deleteChallengeRefundDescription": "Si eliminas este Desafío, recuperarás las Gemas de la recompensa y las tareas del Desafío permanecerán en los listados de tareas de los participantes."
}

View File

@@ -108,5 +108,6 @@
"cannotClone": "Ce Défi ne peut être dupliqué car un·e ou plusieurs joueu·r·se·s l'ont signalé comme inapproprié. Un·e membre de l'équipe va vous contacter sous peu avec des instructions. Si vous n'avez pas de nouvelles après 48 heures, merci d'envoyer un courriel à admin@habitica.com.",
"resetFlags": "Réinitialiser les Signalements",
"cannotMakeChallenge": "Vous ne pouvez pas créer de Défis publics car votre compte n'a pour le moment pas les accès aux discussions. Merci de contacter admin@habitica.com pour plus d'informations.",
"messageChallengeFlagOfficial": "Les Défis Officiels ne peuvent pas être signalés."
"messageChallengeFlagOfficial": "Les Défis Officiels ne peuvent pas être signalés.",
"deleteChallengeRefundDescription": "Si vous supprimez ce Défi, les Gemmes investies vous seront restituées et les Tâches du Défis ne disparaîtront pas des Tâches des participant·e·s."
}

View File

@@ -124,7 +124,7 @@
"passwordReset": "Si nous avons votre courriel ou votre identifiant dans nos fichiers, un nouveau mot de passe vous a été envoyé.",
"invalidLoginCredentialsLong": "Votre email, identifiant ou mot de passe est incorrect. Merci de réessayer ou de faire \"Mot de passe oublié\".",
"invalidCredentials": "Aucun compte n'utilise cet identifiant.",
"accountSuspended": "Ce compte, identifiant “<%= userId %>”, a été bloqué pour avoir enfreint les règles de vie en communauté (https://habitica.com/static/community-guidelines) ou les conditions dutilisation (https://habitica.com/static/terms). Pour plus dinformations, ou pour demander un déblocage, merci denvoyer un courriel à notre gestionnaire de la communauté : <%= communityManagerEmail %>, ou demandez à un parent ou à un tuteur de leur envoyer un courriel. Merci d'y inclure votre @identifiant.",
"accountSuspended": "Votre compte @<%= username %> a été bloqué. Pour plus d'informations ou pour faire appel de la décision, merci de nous contacter à admin@habitica.com en indiquant votre identifiant Habitica ou votre ID d'Utilisat·eur·rice.",
"accountSuspendedTitle": "Le compte a été suspendu",
"unsupportedNetwork": "Ce réseau n'est actuellement pas pris en charge.",
"cantDetachSocial": "Votre compte n'a pas d'autre méthode d'authentification ; vous ne pouvez pas désaffecter cette méthode d'authentification.",

View File

@@ -241,5 +241,6 @@
"titleCustomizations": "Personnalisations",
"rememberToBeKind": "N'oubliez pas d'être bienveillant·e, respectueu·x·se, et de suivre le <a href='/static/community-guidelines' target='_blank'>Guide de la Communauté</a>.",
"newMessage": "Nouveau Message",
"targetUserNotExist": "L'Utilisat·eur·rice '<%= userName %>' n'existe pas."
"targetUserNotExist": "L'Utilisat·eur·rice '<%= userName %>' n'existe pas.",
"gem": "Gemme"
}

View File

@@ -385,15 +385,9 @@
"groupUse": "Qu'est ce qui décrit mieux l'usage de votre groupe ?*",
"groupUseDefault": "Choisissez une réponse",
"createGroup": "Créer un groupe",
"groupParentChildren": "Parent(s) qui définissent des tâches pour les enfants",
"descriptionOptionalText": "Ajouter une description",
"nextPaymentMethod": "Suite : Paiement",
"sendGiftLabel": "Voulez vous envoyer un message avec le cadeau ?",
"groupCouple": "Couple qui partage ses tâches",
"groupFriends": "Amis qui partagent leurs tâches",
"groupCoworkers": "Collaborateurs qui partagent leurs tâches",
"groupManager": "Responsable qui définit des tâches pour ses employés",
"groupTeacher": "Enseignant qui définit des tâches pour les étudiants",
"nameStar": "Nom*",
"nameStarText": "Ajouter un titre",
"descriptionOptional": "Description",

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