Compare commits

..

222 Commits

Author SHA1 Message Date
SabreCat 098f53bfa9 4.244.1 2022-09-21 15:02:39 -05:00
SabreCat 4c988691cf fix(groups): show group task summary modal for site admins
also change "Watcher" to "Peeker" in fall healer gear
2022-09-21 14:39:46 -05:00
SabreCat 1530ab44e9 fix(gala): hide avatar customizations before start date 2022-09-20 16:57:39 -05:00
SabreCat 24430861ce 4.244.0 2022-09-19 16:56:39 -05:00
SabreCat e60285e7d9 Merge branch 'develop' into release 2022-09-19 16:56:35 -05:00
Natalie L f030135c82 chore(content): add 2022 Fall Festival (#14244)
* chore(submodule): add August 2022 Mystery Items

* update(content): add 2022 Fall Festival content

* update(content): add 2022 Fall Festival content

* update(content): update event dates for release

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

* feat(content): remaining descriptions

* fix(event): correct spell availability

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Log social users into matching local auth accounts

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

* If possible set local authentication email for social users

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

* lint fixes

* Fix issues and tests

* fix tests

* Fix lint error.

* purge Facebook.

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

* Fix error

* fix error

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

* fix error

* Fix boolean logic

* fix json conversion

* .

* fix password reset for old social accounts

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

* Log social users into matching local auth accounts

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

* If possible set local authentication email for social users

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

* lint fixes

* Fix issues and tests

* fix tests

* Fix lint error.

* purge Facebook.

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

* Fix error

* fix error

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

* fix error

* Fix boolean logic

* fix json conversion

* fix password reset for old social accounts

* Revert "lint fixes"

This reverts commit c244b1651c.

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

* Revert "fix password reset for old social accounts"

This reverts commit 7e0069a80f.

* fix duplicate code

* chore(misc): remove irrelevant changes

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

Co-authored-by: SabreCat <sabe@habitica.com>
2022-09-15 18:22:52 -05:00
SabreCat 10f5011781 chore(subproject): update habitica-images SHA 2022-09-15 11:35:27 -05:00
Natalie L 9a896470d5 Site Footer Update (#14134)
* first pass

* second pass

* third pass

* fourth pass, not a lot of progress

* fifth pass, slowly gettin there

* breakpoint nonsense

* flexbox and breakpoint nonsense

* svg updates, css updates

* whitespace

* chore: merge upstream/develop

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

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

* style: mobile footer

* style: mobile footer

* style: static landing page footer

* style: small tweaks to social layout

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

* update(style): small updates

* update(style): cleaning up code

* update(style): cleaning up code

* update(style): mobile footer updates

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

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

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

* fix(string): remove extra word from headSpecialSummer2022WarriorNotes

* fix(string): corrected armorSpecialSummer2022MageNotes

* fix: remove duplicated string and adjust upgrade button style

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

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

Co-authored-by: SabreCat <sabe@habitica.com>
2022-09-14 16:33:31 -05:00
Weblate 5f440f1bfa Merge branch 'origin/develop' into Weblate. 2022-09-13 17:11:21 +02:00
SabreCat d4f9555f11 4.243.1 2022-09-13 10:11:17 -05:00
Weblate 203d97423a Translated using Weblate (Russian)
Currently translated at 97.9% (381 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Danish)

Currently translated at 94.0% (127 of 135 strings)

Translated using Weblate (Danish)

Currently translated at 89.3% (84 of 94 strings)

Translated using Weblate (Danish)

Currently translated at 97.2% (175 of 180 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Danish)

Currently translated at 86.0% (185 of 215 strings)

Translated using Weblate (Danish)

Currently translated at 93.3% (126 of 135 strings)

Translated using Weblate (Danish)

Currently translated at 71.0% (147 of 207 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Danish)

Currently translated at 88.2% (83 of 94 strings)

Translated using Weblate (Danish)

Currently translated at 99.0% (110 of 111 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Danish)

Currently translated at 99.1% (226 of 228 strings)

Translated using Weblate (Danish)

Currently translated at 78.9% (307 of 389 strings)

Translated using Weblate (Danish)

Currently translated at 68.6% (1829 of 2663 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Danish)

Currently translated at 96.6% (174 of 180 strings)

Translated using Weblate (Danish)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Danish)

Currently translated at 81.1% (613 of 755 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Danish)

Currently translated at 67.8% (492 of 725 strings)

Translated using Weblate (Danish)

Currently translated at 85.5% (184 of 215 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.9% (611 of 2663 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (379 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.7% (330 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 42.7% (323 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.8% (316 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.7% (315 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (206 of 207 strings)

Translated using Weblate (Russian)

Currently translated at 95.6% (372 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Danish)

Currently translated at 98.6% (225 of 228 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (German)

Currently translated at 92.5% (360 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2663 of 2663 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (2562 of 2663 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

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

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

* fix(date): fix start date
2022-09-13 09:12:24 -05:00
SabreCat 38bf0b3721 4.243.0 2022-09-06 12:35:18 -05:00
SabreCat 88c8b545f4 Merge branch 'develop' into release 2022-09-06 12:35:14 -05:00
Weblate 184ee7262e Translated using Weblate (Japanese)
Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.3% (312 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.3% (158 of 207 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.8% (157 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (French)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.6% (125 of 128 strings)

Translated using Weblate (French)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2653 of 2659 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.1% (311 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (German)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Danish)

Currently translated at 80.5% (608 of 755 strings)

Translated using Weblate (Danish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Danish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Chinese (Hong Kong))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.2% (133 of 207 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Spanish)

Currently translated at 96.3% (2562 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 79.3% (570 of 718 strings)

Translated using Weblate (German)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2652 of 2659 strings)

Translated using Weblate (German)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.2% (359 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (206 of 207 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (127 of 128 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (376 of 389 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (376 of 389 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2655 of 2659 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (207 of 207 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (128 of 128 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2659 of 2659 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (128 of 128 strings)

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

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

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

* fix(typo): consistent caps

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-09-06 12:31:55 -05:00
SabreCat e383614107 4.242.1 2022-09-01 14:47:20 -05:00
Natalie L 2b21410abd fix(quest shop): correct forest friends end date (#14202) 2022-09-01 14:45:49 -05:00
Weblate 56f956be5a Merge branch 'origin/develop' into Weblate. 2022-08-30 21:25:56 +02:00
Weblate 2b44d32b1c Translated using Weblate (Ukrainian)
Currently translated at 39.3% (81 of 206 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.1% (311 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 94.8% (369 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.9% (118 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 78.6% (565 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.4% (174 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 64.0% (132 of 206 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

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

Currently translated at 88.5% (124 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 43.8% (79 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 41.1% (74 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (30 of 58 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Korean)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (561 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 63.5% (131 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Czech)

Currently translated at 74.4% (1977 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.7% (100 of 127 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (French)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 92.5% (360 of 389 strings)

Translated using Weblate (French)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Czech)

Currently translated at 74.2% (1972 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 98.9% (93 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 73.8% (1962 of 2655 strings)

Translated using Weblate (Romanian)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Czech)

Currently translated at 72.8% (1934 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 92.8% (361 of 389 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Romanian)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Romanian)

Currently translated at 90.0% (126 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (389 of 389 strings)

Translated using Weblate (Italian)

Currently translated at 99.2% (386 of 389 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (2649 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 93.5% (131 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (354 of 367 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Filipino)

Currently translated at 81.3% (109 of 134 strings)

Translated using Weblate (Filipino)

Currently translated at 95.0% (133 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 36.3% (8 of 22 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.8% (214 of 228 strings)

Translated using Weblate (Spanish)

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.4% (204 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (2642 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (2640 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 50.0% (11 of 22 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 22.6% (602 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (2636 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.6% (786 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Filipino)

Currently translated at 72.9% (551 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 96.4% (135 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 93.4% (200 of 214 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.8% (1163 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.8% (198 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (2628 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.0% (108 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 57.8% (1535 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2623 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.4% (197 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.7% (1667 of 2655 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 72.8% (550 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 72.1% (101 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.1% (56 of 206 strings)

Translated using Weblate (French)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.4% (1951 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 40.9% (309 of 755 strings)

Translated using Weblate (French)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Korean)

Currently translated at 92.3% (339 of 367 strings)

Translated using Weblate (Filipino)

Currently translated at 86.3% (620 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 74.7% (564 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 74.2% (104 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.6% (96 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.5% (2005 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 75.3% (569 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 78.5% (110 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 33.4% (889 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Dutch)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.9% (490 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.3% (554 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.8% (148 of 206 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.9% (180 of 228 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.7% (2010 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (2606 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.1% (173 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.5% (44 of 56 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.6% (594 of 755 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Ukrainian)

Currently translated at 59.0% (75 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (114 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 90.0% (162 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 97.6% (209 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (206 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (108 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 86.1% (316 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1783 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 72.2% (39 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 91.4% (43 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 92.2% (344 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 78.6% (169 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 92.8% (52 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (107 of 129 strings)

Translated using Weblate (Galician)

Currently translated at 94.6% (176 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.8% (119 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 65.9% (62 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (4 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 12.1% (17 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 83.6% (51 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 77.2% (17 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 54.8% (125 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 85.8% (315 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1782 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 82.9% (39 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 70.8% (90 of 127 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (137 of 140 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (138 of 140 strings)

Translated using Weblate (Korean)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (205 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2590 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (2589 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Filipino)

Currently translated at 84.6% (94 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 79.2% (111 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 94.1% (351 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 86.4% (96 of 111 strings)

Translated using Weblate (Filipino)

Currently translated at 82.1% (115 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 82.8% (116 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.4% (172 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 83.5% (117 of 140 strings)

Translated using Weblate (Filipino)

Currently translated at 83.9% (173 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (903 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2588 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (French)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 62.1% (128 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2587 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.4% (120 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.6% (2647 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 88.9% (113 of 127 strings)

Translated using Weblate (Korean)

Currently translated at 79.5% (101 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 97.4% (2586 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2583 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 97.2% (2581 of 2655 strings)

Translated using Weblate (Filipino)

Currently translated at 87.3% (180 of 206 strings)

Translated using Weblate (French)

Currently translated at 99.3% (2639 of 2655 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Russian)

Currently translated at 97.0% (2576 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (180 of 180 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (228 of 228 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Filipino)

Currently translated at 86.8% (179 of 206 strings)

Translated using Weblate (Filipino)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Korean)

Currently translated at 74.8% (95 of 127 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2632 of 2655 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 97.7% (702 of 718 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 34.0% (904 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (French)

Currently translated at 98.7% (2622 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 96.7% (2568 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (German)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2652 of 2655 strings)

Translated using Weblate (French)

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 82.4% (113 of 137 strings)

Translated using Weblate (Filipino)

Currently translated at 42.3% (1125 of 2655 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 38.3% (69 of 180 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 94.7% (127 of 134 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 90.1% (193 of 214 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 71.6% (91 of 127 strings)

Translated using Weblate (Russian)

Currently translated at 96.5% (2563 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (214 of 215 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (2559 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 85.1% (46 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 78.1% (107 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Korean)

Currently translated at 64.5% (1713 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Korean)

Currently translated at 89.9% (116 of 129 strings)

Translated using Weblate (Korean)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (618 of 755 strings)

Translated using Weblate (Korean)

Currently translated at 76.6% (105 of 137 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 92.6% (340 of 367 strings)

Translated using Weblate (Korean)

Currently translated at 64.4% (1711 of 2655 strings)

Translated using Weblate (Korean)

Currently translated at 83.3% (45 of 54 strings)

Translated using Weblate (Korean)

Currently translated at 77.9% (560 of 718 strings)

Translated using Weblate (Korean)

Currently translated at 81.4% (44 of 54 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Japanese)

Currently translated at 99.0% (213 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 44.6% (25 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 95.6% (178 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.7% (625 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 67.1% (1784 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 83.7% (93 of 111 strings)

Translated using Weblate (Galician)

Currently translated at 21.1% (38 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 96.2% (179 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 58.3% (419 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 89.5% (120 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 59.2% (122 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 83.1% (628 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 13.8% (19 of 137 strings)

Translated using Weblate (Galician)

Currently translated at 56.1% (128 of 228 strings)

Translated using Weblate (Galician)

Currently translated at 87.1% (320 of 367 strings)

Translated using Weblate (Galician)

Currently translated at 92.9% (199 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 67.3% (1789 of 2655 strings)

Translated using Weblate (Galician)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 89.8% (335 of 373 strings)

Translated using Weblate (Galician)

Currently translated at 74.0% (94 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 76.7% (165 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 3.8% (7 of 180 strings)

Translated using Weblate (Galician)

Currently translated at 96.4% (54 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 50.0% (28 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 57.1% (410 of 718 strings)

Translated using Weblate (Galician)

Currently translated at 88.0% (118 of 134 strings)

Translated using Weblate (Galician)

Currently translated at 58.7% (121 of 206 strings)

Translated using Weblate (Galician)

Currently translated at 82.1% (620 of 755 strings)

Translated using Weblate (Galician)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 91.1% (195 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 73.2% (93 of 127 strings)

Translated using Weblate (Galician)

Currently translated at 74.4% (160 of 215 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (373 of 373 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (German)

Currently translated at 99.8% (2651 of 2655 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (754 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Korean)

Currently translated at 75.9% (104 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (206 of 206 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2655 of 2655 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

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

* chore(content): add September 2022 Mystery Items

* fix(typo): verb form

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

Tests:

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

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

* differenciate insufficient mana and disabled spell

* linting

* reduce opacity, no hover state when insufficient mana

* display that lvl insufficient in spell tooltip

* change spell text color when spell has no effect

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

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

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

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

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

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

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

* chore: update habitica-images

* chore(content): add Porcelain Magic Hatching Potion

* chore(content): update moonglow potion availability

* fix(events): no gap between events

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-15 13:06:55 -04:00
SabreCat 1701fc702b Merge branch 'develop' into sabrecat/teams-rebase 2022-08-09 11:55:46 -05:00
SabreCat 16dc6a1b4c Merge branch 'release' into develop 2022-08-09 11:54:45 -05:00
SabreCat d3a91aab72 4.239.0 2022-08-09 11:52:26 -05:00
Natalie L 0528ee1761 fix(dates): update end dates for quest bundle (#14167)
* chore(submodule): add August 2022 Mystery Items

* chore(content): add Woodland Wizard achievement

* chore(content): add Forest Friends quest bundle

* fix(typo): whitespace

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

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2022-08-09 11:50:05 -05:00
SabreCat 1b4d670b0a fix(tasks): styles and wordings 2022-08-08 15:50:37 -05:00
SabreCat 3654e01fee Merge branch 'develop' into sabrecat/teams-rebase 2022-08-08 12:13:29 -05:00
SabreCat fdbeda19e2 fix(tasks): icons and select lists 2022-08-08 12:13:15 -05:00
dependabot[bot] db723d79a4 build(deps): bump dompurify from 2.3.8 to 2.3.10 in /website/client (#14132)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.8 to 2.3.10.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.8...2.3.10)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

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

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 354 B

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

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 914 B

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

After

Width:  |  Height:  |  Size: 325 B

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

After

Width:  |  Height:  |  Size: 516 B

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

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 622 B

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

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 569 B

File diff suppressed because it is too large Load Diff
@@ -1,21 +1,5 @@
<template>
<div class="form">
<div class="form-group row text-center">
<div class="col-12">
<div
class="btn btn-secondary social-button"
@click="socialAuth('facebook')"
>
<div
class="svg-icon social-icon"
v-html="icons.facebookIcon"
></div>
<span>{{ registering
? $t('signUpWithSocial', {social: 'Facebook'})
: $t('loginWithSocial', {social: 'Facebook'}) }}</span>
</div>
</div>
</div>
<div class="form-group row text-center">
<div class="col-12">
<div
@@ -243,7 +227,6 @@ import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
@@ -260,7 +243,6 @@ export default {
};
data.icons = Object.freeze({
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
@@ -308,8 +290,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
@@ -621,7 +621,6 @@ import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import exclamation from '@/assets/svg/exclamation.svg';
import gryphon from '@/assets/svg/gryphon.svg';
import habiticaIcon from '@/assets/svg/habitica-logo.svg';
import facebookSquareIcon from '@/assets/svg/facebook-square.svg';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
@@ -644,7 +643,6 @@ export default {
exclamation,
gryphon,
habiticaIcon,
facebookIcon: facebookSquareIcon,
googleIcon,
appleIcon,
});
@@ -734,8 +732,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
},
@@ -757,8 +753,7 @@ export default {
}, 500),
sanitizeRedirect (redirect) {
if (!redirect) return '/';
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
sanitizedString = `/${sanitizedString}`;
const sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
return sanitizedString;
},
async register () {
@@ -27,7 +27,7 @@
<customize-options
:items="set.options"
:current-value="user.preferences.hair.color"
:full-set="!hideSet(set) && !userOwnsSet('hair', set.keys, 'color')"
:full-set="!hideSet(set.key) && !userOwnsSet('hair', set.keys, 'color')"
@unlock="unlock(`hair.color.${set.keys.join(',hair.color.')}`)"
/>
</div>
@@ -23,7 +23,7 @@
<customize-options
:items="set.options"
:current-value="user.preferences.skin"
:full-set="!hideSet(set) && !userOwnsSet('skin', set.keys)"
:full-set="!hideSet(set.key) && !userOwnsSet('skin', set.keys)"
@unlock="unlock(`skin.${set.keys.join(',skin.')}`)"
/>
</div>
@@ -112,10 +112,8 @@ export default {
},
},
watch: {
'group._id': {
async groupId () {
this.loadChallenges();
},
'group._id': function groupId () {
this.loadChallenges();
},
},
mounted () {
@@ -0,0 +1,204 @@
<template>
<b-modal
id="group-plans-update"
title="New Shared Task Board"
size="lg"
hide-footer="hide-footer"
:no-close-on-backdrop="true"
:no-close-on-esc="true"
:centered="true"
>
<div
slot="modal-header"
class="w-100 d-flex pt-2 justify-content-center"
>
<h2
class="mx-auto mt-4"
v-once
>
{{ $t('newGroupsWelcome') }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
aria-hidden="true"
tabindex="0"
@click="close()"
@keypress.enter="close()"
v-html="icons.close"
></div>
<img
class="task-columns"
src="~@/assets/images/group-plans/task-columns.png"
srcset="~@/assets/images/group-plans/task-columns@2x.png 2x,
~@/assets/images/group-plans/task-columns@3x.png 3x"
>
</div>
<div
class="d-flex flex-column justify-content-center p-4"
>
<div class="d-flex align-items-center justify-content-center mb-3">
<div
class="svg-icon sparkles my-auto mr-3"
v-html="icons.sparkles"
>
</div>
<h3
class="my-auto"
v-once
>
{{ $t('newGroupsWhatsNew') }}
</h3>
<div
class="svg-icon sparkles my-auto ml-3 flip-x"
v-html="icons.sparkles"
>
</div>
</div>
<ul
class="mb-4 px-4"
>
<li>{{ $t('newGroupsBullet01') }}</li>
<li>{{ $t('newGroupsBullet02') }}</li>
<li>{{ $t('newGroupsBullet03') }}</li>
<li>{{ $t('newGroupsBullet04') }}</li>
<li>{{ $t('newGroupsBullet05') }}</li>
<li>{{ $t('newGroupsBullet06') }}</li>
<li>{{ $t('newGroupsBullet07') }}</li>
<li>{{ $t('newGroupsBullet08') }}</li>
<li>{{ $t('newGroupsBullet09') }}</li>
<li>{{ $t('newGroupsBullet10') }}
<ul class="p-0">
<li v-html="$t('newGroupsBullet10a')"></li>
<li v-html="$t('newGroupsBullet10b')"></li>
<li v-html="$t('newGroupsBullet10c')"></li>
</ul>
</li>
</ul>
<div
class="mx-auto"
v-html="$t('newGroupsVisitFAQ')"
></div>
<div
class="mx-auto"
>
{{ $t('newGroupsEnjoy') }}
</div>
<button
class="btn btn-primary mt-4 mb-1 mx-auto"
@click="close()"
@keypress.enter="close()"
>
{{ $t('getStarted') }}
</button>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#group-plans-update {
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-dialog {
max-width: 566px;
margin-top: 0px;
}
.modal-header {
background-image: linear-gradient(64deg, $purple-200, $purple-300 55%);
height: 136px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h2 {
color: $white;
line-height: 28px;
}
li {
padding-left: 0.75rem;
}
ul {
line-height: 24px;
list-style-type: square;
li ul {
list-style-type: none;
li::before {
margin-right: 1rem;
font-size: 16px;
content: '▫';
}
}
}
.btn-primary {
width: max-content;
}
.close-x {
color: $purple-400;
height: 16px;
width: 16px;
position: absolute;
opacity: 0.75;
cursor: pointer;
right: 18px;
top: 18px;
&:hover, &:focus {
opacity: 1;
}
}
.sparkles {
width: 32px;
height: 17px;
&.flip-x {
transform: scaleX(-1);
}
}
.task-columns {
position: absolute;
top: 84px;
}
</style>
<script>
import closeIcon from '@/assets/svg/close.svg';
import sparkles from '@/assets/svg/sparkles-left.svg';
export default {
data () {
return {
icons: Object.freeze({
close: closeIcon,
sparkles,
}),
};
},
methods: {
close () {
this.$store.dispatch('user:set', { 'flags.tour.groupPlans': -2 });
this.$root.$emit('bv::hide::modal', 'group-plans-update');
},
},
};
</script>
@@ -28,7 +28,7 @@
{{ $t('groupBilling') }}
</router-link>
</secondary-menu>
<div class="col-12">
<div class="col-12 px-0">
<router-view />
</div>
</div>
@@ -1,5 +1,8 @@
<template>
<div class="standard-page">
<div
class="standard-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<group-plan-overview-modal />
<task-modal
ref="taskModal"
@@ -7,65 +10,80 @@
:purpose="taskFormPurpose"
:group-id="groupId"
@cancel="cancelTaskModal()"
@taskCreated="taskCreated"
@taskEdited="taskEdited"
@taskCreated="loadTasks"
@taskEdited="loadTasks"
@taskDestroyed="taskDestroyed"
/>
<div class="row tasks-navigation">
<div class="col-12 col-md-4">
<h1>{{ $t('groupTasksTitle') }}</h1>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="d-flex flex-wrap align-items-center mb-4">
<div class="mr-auto">
<h1>{{ group.name }}</h1>
</div>
<!-- @TODO: Abstract to component!-->
<div class="col-12 col-md-4">
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
</div>
<div
v-if="canCreateTasks"
class="create-task-area d-flex"
<input
v-model="searchText"
class="form-control input-search"
type="text"
:placeholder="$t('search')"
>
<transition name="slide-tasks-btns">
<div
class="d-flex flex-wrap align-items-center justify-content-end ml-auto"
>
<toggle-switch
id="taskMirrorToggle"
class="mr-3 mb-1 ml-auto"
:label="'Copy tasks'"
:checked="user.preferences.tasks.mirrorGroupTasks.indexOf(group._id) !== -1"
:hover-text="'Show assigned and open tasks on your personal task board'"
@change="changeMirrorPreference"
/>
<div
class="day-start d-flex align-items-center"
v-html="$t('dayStart', { startTime: groupStartTime } )"
>
</div>
<div class="ml-2">
<button
id="create-task-btn"
v-if="canCreateTasks"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</button>
<div
v-if="openCreateBtn"
class="d-flex"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
class="svg-icon"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('create') }}
</b-tooltip>
</div>
</div>
<div class="row">
@@ -79,6 +97,7 @@
:search-text="searchText"
:draggable-override="canCreateTasks"
@editTask="editTask"
@taskSummary="taskSummary"
@loadGroupCompletedTodos="loadGroupCompletedTodos"
@taskDestroyed="taskDestroyed"
/>
@@ -86,12 +105,58 @@
</div>
</template>
<style lang="scss">
#taskMirrorToggle {
font-weight: bold;
.svg-icon {
margin: 3px 6px 0px 4px;
}
.toggle-switch {
margin-left: 0px;
}
.toggle-switch-description {
margin-top: 3px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/create-task.scss';
.tasks-navigation {
margin-bottom: 40px;
h1 {
color: $purple-300;
margin-bottom: 0px;
}
.day-start {
height: 2rem;
padding: 0.25rem 0.75rem;
border-radius: 2px;
color: $gray-100;
background-color: $gray-600;
}
@media screen and (min-width: 1200px) {
.input-search {
margin-left: 12.5rem;
width: 25%;
}
}
@media screen and (max-width: 1200px) {
.input-search {
width: 50%;
}
}
@media screen and (max-width: 500px) {
#create-task-btn {
margin-top: 4px;
}
}
.positive {
@@ -107,11 +172,15 @@
import Vue from 'vue';
import cloneDeep from 'lodash/cloneDeep';
import findIndex from 'lodash/findIndex';
import groupBy from 'lodash/groupBy';
import moment from 'moment';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import TaskColumn from '../tasks/column';
import TaskModal from '../tasks/taskModal';
import TaskSummary from '../tasks/taskSummary';
import GroupPlanOverviewModal from './groupPlanOverviewModal';
import toggleSwitch from '@/components/ui/toggleSwitch';
import sync from '../../mixins/sync';
import positiveIcon from '@/assets/svg/positive.svg';
import filterIcon from '@/assets/svg/filter.svg';
@@ -121,14 +190,18 @@ import dailyIcon from '@/assets/svg/daily.svg';
import todoIcon from '@/assets/svg/todo.svg';
import rewardIcon from '@/assets/svg/reward.svg';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
GroupPlanOverviewModal,
toggleSwitch,
},
mixins: [sync],
props: ['groupId'],
data () {
return {
@@ -199,6 +272,16 @@ export default {
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
groupStartTime () {
if (!this.group || !this.group.cron) return null;
const { dayStart, timezoneOffset } = this.group.cron;
const timezoneDiff = this.user.preferences.timezoneOffset - timezoneOffset;
return moment()
.hour(dayStart)
.minute(0)
.subtract(timezoneDiff, 'minutes')
.format('h:mm A');
},
},
watch: {
// call again the method if the route changes (when this route is already active)
@@ -208,6 +291,10 @@ export default {
this.$set(this, 'searchId', to.params.groupId);
next();
},
async beforeRouteLeave (to, from, next) {
await this.sync();
next();
},
mounted () {
if (!this.searchId) this.searchId = this.groupId;
this.load();
@@ -218,6 +305,7 @@ export default {
this.$root.$on('habitica:team-sync', () => {
this.loadTasks();
this.loadGroupCompletedTodos();
});
},
methods: {
@@ -238,6 +326,9 @@ export default {
this.group.members = members;
this.loadTasks();
if (this.user.flags.tour.groupPlans !== -2) {
this.$root.$emit('bv::show::modal', 'group-plans-update');
}
},
async loadTasks () {
this.tasksByType = {
@@ -251,22 +342,13 @@ export default {
groupId: this.searchId,
});
const groupedApprovals = await this.loadApprovals();
tasks.forEach(task => {
if (
groupedApprovals[task._id]
&& groupedApprovals[task._id].length > 0
) task.approvals = groupedApprovals[task._id];
this.tasksByType[task.type].push(task);
});
},
async loadApprovals () {
const approvalRequests = await this.$store.dispatch('tasks:getGroupApprovals', {
groupId: this.searchId,
});
return groupBy(approvalRequests, 'group.taskId');
if (this.editingTask && this.editingTask.completed) {
this.loadGroupCompletedTodos();
}
},
editTask (task) {
this.taskFormPurpose = 'edit';
@@ -277,6 +359,12 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
async loadGroupCompletedTodos () {
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
groupId: this.searchId,
@@ -299,14 +387,6 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskCreated (task) {
task.group.id = this.group._id;
this.tasksByType[task.type].unshift(task);
},
taskEdited (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1, task);
},
taskDestroyed (task) {
const index = findIndex(this.tasksByType[task.type], taskItem => taskItem._id === task._id);
this.tasksByType[task.type].splice(index, 1);
@@ -354,6 +434,25 @@ export default {
if (this.temporarilySelectedTags.indexOf(tagId) !== -1) return true;
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
} else { // we're turning copy OFF for this group
groupsToMirror.splice(groupsToMirror.indexOf(this.group._id), 1);
}
this.$store.dispatch('user:set', {
'preferences.tasks.mirrorGroupTasks': groupsToMirror,
});
},
},
};
</script>
@@ -92,7 +92,6 @@
</div>
</div>
<div class="box payment-providers">
<h3>Choose your payment method</h3>
<payments-buttons
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
:amazon-data="pay(PAYMENTS.AMAZON)"
@@ -241,7 +240,6 @@
class="col-12"
>
<div class="text-center">
<h3>Choose your payment method</h3>
<payments-buttons
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
:amazon-data="pay(PAYMENTS.AMAZON)"
@@ -266,6 +264,11 @@
.purple-box {
color: #bda8ff;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
}
.number {
@@ -279,6 +282,8 @@
.payment-providers {
width: 350px;
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
}
@@ -398,6 +403,10 @@
.payment-options {
margin-bottom: 4em;
h4 {
color: #34313a;
}
.purple-box {
background-color: #4f2a93;
color: #fff;
@@ -247,9 +247,6 @@
<li v-once>
{{ $t('sleepBullet3') }}
</li>
<li v-once>
{{ $t('sleepBullet4') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
@@ -342,6 +339,7 @@
<li>
<a
href
:style="glossary-link"
v-html="$t('glossary')"
></a>
</li>
@@ -536,6 +534,21 @@
margin-left: .5em;
}
// formats the report a bug link to match the others
a:not([href]) {
&:not([role=button]) {
color: #007bff;
text-decoration: none;
}
}
a:not([href]):hover {
&:not([role=button]) {
color: #0056b3;
text-decoration: underline;
}
}
.tier1-icon, .tier2-icon {
width: 11px;
}
@@ -23,7 +23,14 @@ export default {
props: ['notification', 'canRemove'],
methods: {
action () {
this.$router.push({ name: 'tasks' });
if (this.notification.data.groupId) {
this.$router.push({
name: 'groupPlanDetailTaskInformation',
params: { groupId: this.notification.data.groupId },
});
} else {
this.$router.push({ name: 'tasks' });
}
},
},
};
@@ -129,8 +129,6 @@ import GUILD_INVITATION from './notifications/guildInvitation';
import PARTY_INVITATION from './notifications/partyInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import QUEST_INVITATION from './notifications/questInvitation';
import GROUP_TASK_APPROVAL from './notifications/groupTaskApproval';
import GROUP_TASK_APPROVED from './notifications/groupTaskApproved';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
@@ -155,8 +153,6 @@ export default {
PARTY_INVITATION,
CHALLENGE_INVITATION,
QUEST_INVITATION,
GROUP_TASK_APPROVAL,
GROUP_TASK_APPROVED,
GROUP_TASK_ASSIGNED,
GROUP_TASK_CLAIMED,
UNALLOCATED_STATS_POINTS,
@@ -182,7 +178,7 @@ export default {
openStatus: undefined,
actionableNotifications: [
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
'QUEST_INVITATION', 'GROUP_TASK_APPROVED',
'QUEST_INVITATION',
],
// A list of notifications handled by this component,
// listed in the order they should appear in the notifications panel.
@@ -196,8 +192,6 @@ export default {
'CHALLENGE_INVITATION',
'QUEST_INVITATION',
'GROUP_TASK_ASSIGNED',
'GROUP_TASK_APPROVAL',
'GROUP_TASK_APPROVED',
'GROUP_TASK_CLAIMED',
'NEW_MYSTERY_ITEMS',
'CARD_RECEIVED',
@@ -32,11 +32,12 @@
/>
<onboarding-complete />
<first-drops />
<group-plans-update />
</div>
</template>
<style lang='scss'>
.introjs-helperNumberLayer, .introjs-bullets {
.introjs-helperNumberLayer, .introjs-bullets, .introjs-skipbutton {
display: none;
}
@@ -62,9 +63,9 @@
.npc_justin_textbox {
position: absolute;
right: 1em;
top: -3.6em;
top: -55px;
width: 48px;
height: 52px;
height: 51px;
background-image: url('~@/assets/images/justin_textbox.png');
}
}
@@ -95,7 +96,7 @@
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12) !important;
}
.introjs-skipbutton.btn-primary, .introjs-donebutton.btn-primary {
.introjs-donebutton.btn-primary {
color: #fff;
}
</style>
@@ -142,6 +143,7 @@ import loginIncentives from './achievements/login-incentives';
import onboardingComplete from './achievements/onboardingComplete';
import verifyUsername from './settings/verifyUsername';
import firstDrops from './achievements/firstDrops';
import groupPlansUpdate from './group-plans/groupPlansUpdateModal';
const NOTIFICATIONS = {
// general notifications
@@ -268,6 +270,7 @@ export default {
genericAchievement,
onboardingComplete,
firstDrops,
groupPlansUpdate,
},
mixins: [notifications, guide],
data () {
@@ -612,7 +615,7 @@ export default {
const yesterUtcOffset = yesterDay.utcOffset();
dailys.forEach(task => {
if (task && task.group.approval && task.group.approval.requested) return;
if (task.group && task.group.id) return;
if (task.completed) return;
const due = shouldDo(yesterDay, task, { timezoneUtcOffset: yesterUtcOffset });
if (task.yesterDaily && due) this.yesterDailies.push(task);
@@ -153,14 +153,6 @@
</span>
</div>
</div>
<div class="row text-center">
<h4
v-once
class="col-12 text-payment mb-3"
>
{{ $t('choosePaymentMethod') }}
</h4>
</div>
<payments-buttons
:disabled="!selectedGemsBlock"
:stripe-fn="() => redirectToStripe({ gemsBlock: selectedGemsBlock })"
@@ -387,9 +387,7 @@
{{ $t('saveAndConfirm') }}
</button>
</div>
<h5
v-if="user.auth.local.email"
>
<h5 v-if="user.auth.local.has_password">
{{ $t('changeEmail') }}
</h5>
<div
+1 -5
View File
@@ -94,6 +94,7 @@ export default {
'gems',
'bugs-features',
'world-boss',
'group-plans',
];
const hash = window.location.hash.replace('#', '');
@@ -105,11 +106,6 @@ export default {
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
},
visible: hash && headings.includes(hash) ? hash : null,
// @TODO webFaqStillNeedHelp: {
// linkStart: '[',
// linkEnd: '](/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)',
// },
// "webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the <%= linkStart %>Habitica Help guild<%= linkEnd %>! We're happy to help."
};
},
mounted () {
@@ -3,7 +3,7 @@
<div class="container-fluid">
<h1>Privacy Notice</h1>
<p class="strong pagemeta">
Last Updated: December 10, 2021
Last Updated September 19, 2022: Removed reference to Facebook login, which is no longer supported.
</p>
<p>
HabitRPG, Inc. (HabitRPG, we, us, or our) welcomes you. This privacy notice (the Privacy
@@ -39,8 +39,8 @@
In connection with the creation of an account on our Platforms, we collect account credentials such as
your email, username, and password. We use this account information to create your account, including to
verify your identity. We also use this information to manage your account, including your transactions. If
you choose to log into your account through Google, Apple or Facebook, we capture and store the User
ID and email address connected to the respective account, so we can verify your identity when you log in.
you choose to log into your account through Google or Apple, we capture and store the User ID and email
address connected to the respective account, so we can verify your identity when you log in.
</p>
<h3>User Content</h3>
<p>
@@ -33,7 +33,7 @@
@import '~@/assets/scss/colors.scss';
.home-header {
background: #6133b4 !important;
background: $purple-300 !important;
position: static;
box-shadow: none !important;
height: 100px !important;
@@ -46,13 +46,13 @@
.nav-item a {
font-size: 14px !important;
color: #d5c8ff !important;
color: $purple-600 !important;
padding-top: 2.8em !important;
}
.nav-item a:hover {
background: transparent !important;
color: #fff !important;
color: $white !important;
}
.nav-item .nav-link {
@@ -73,20 +73,20 @@
}
.white-header {
background: #fff !important;
background-color: #fff !important;
background: $white !important;
background-color: $white !important;
a {
color: #271b3d !important;
color: $header-dark-background !important;
}
a:hover {
color: #fff !important;
color: $white !important;
}
}
#purple-footer {
background-color: #271b3d;
background-color: $header-dark-background;
.row {
margin: 0;
@@ -94,19 +94,91 @@
footer, footer a {
background: transparent;
color: #d5c8ff;
color: $purple-500;
&:hover {
color: $white;
}
}
h3 {
color: $purple-400;
}
hr {
border-top-color: $purple-100;
}
.donate-text {
color: $purple-500;
}
.logo {
color: #bda8ff;
color: $purple-300;
}
.social-circle, .btn-contribute {
background: #36205d;
color: #bda8ff;
.colophon {
color: $purple-500;
}
.svg-icon {
color: #bda8ff;
.social-circle {
background: $purple-50;
color: $purple-500;
.instagram svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.twitter svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.facebook svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.tumblr svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
}
.btn-contribute {
background: $white;
box-shadow: none;
border-radius: 2px;
width: 175px;
height: 32px;
color: $gray-50;
text-align: center;
line-height: 1.71;
font-weight: bold;
font-size: 0.875rem;
vertical-align: middle;
padding: 0;
margin: 32px 0 32px 24px;
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
a {
display: flex;
}
.text{
display: inline-block;
vertical-align: bottom;
}
}
}
@@ -142,7 +214,7 @@
<style lang="scss" scoped>
#bottom-wrap.purple-4 {
background-color: #271b3d;
background-color: #271B3D;
}
#bottom-wrap {
@@ -1,59 +1,117 @@
<template>
<div>
<approval-modal :task="task" />
<div
class="gray-100"
>
<div
v-if="showStatus"
>
<div
v-for="completion in completionsList"
:key="completion.userId"
class="d-flex completion-row"
>
<div class="control">
<div
class="inner d-flex justify-content-center align-items-center p-auto"
:class="{interactive: iconClass(completion) !== 'lock'}"
@click="iconClass(completion) !== 'lock' ? needsWork(completion) : null"
>
<div
class="icon"
:class="iconClass(completion)"
v-html="icons[iconClass(completion)]"
>
</div>
</div>
</div>
<div
class="px-75 py-2 info"
>
<div>
<strong> @{{ completion.userName }} </strong>
</div>
<div
v-if='completion.completedDate'
:class="{'green-10': completion.completed}"
>
{{ completion.completedDateString }}
</div>
</div>
</div>
</div>
<div
v-if="!(userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit')"
class="claim-bottom-message d-flex align-items-center"
>
<div
class="mr-auto ml-2"
v-if="assignedUsersCount > 0"
class="svg-icon ml-2 users-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.users"
></div>
<div
class="mr-auto ml-1 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="message"
></div>
<div
v-if="!userIsAssigned && !task.completed"
class="ml-auto mr-2"
class="d-flex ml-auto mr-1 my-auto"
v-if="task.group.assignedUsers && ['daily','todo'].indexOf(task.type) !== -1"
>
<a
class="claim-color"
@click="claim()"
>{{ $t('claim') }}</a>
<span
v-if="assignedUsersCount > 1"
class="d-flex mr-1 my-auto"
>
<span
class="small-check"
v-if="!showStatus && completionsCount"
>
<div
class="svg-icon color"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-html="icons.check"
>
</div>
</span>
<span
class="ml-1 mr-2 my-auto"
:class="{'green-10': completionsCount === assignedUsersCount}"
v-if="!showStatus && completionsCount"
>
{{ completionsCount }}/{{ assignedUsersCount }}
</span>
<a
v-if="assignedUsersCount > 1 && !showStatus"
class="blue-10"
@click="showStatus = !showStatus"
>
{{ $t('viewStatus') }}
</a>
<a
v-if="showStatus"
@click="showStatus = !showStatus"
>
{{ $t('close') }}
</a>
</span>
<span
v-if="assignedUsersCount === 1 && task.type === 'daily'
&& !task.completed && singleAssignLastDone"
class="mr-1 d-inline-flex"
>
<span
v-html="icons.lastComplete"
v-b-tooltip.hover.bottom="$t('lastCompleted')"
class="svg-icon color last-completed mr-1 my-auto"
:class="{'gray-200': completionsCount !== assignedUsersCount}"
>
</span>
<span
:class="{'green-10': completionsCount === assignedUsersCount}"
>
{{ formattedCompletionTime }}
</span>
</span>
</div>
<div
v-if="userIsAssigned && !approvalRequested && !task.completed"
class="ml-auto mr-2"
>
<a
class="unclaim-color"
@click="unassign()"
>{{ $t('removeClaim') }}</a>
</div>
</div>
<div
v-if="approvalRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="approve()"
>{{ $t('approveTask') }}</a>
<a @click="needsWork()">{{ $t('needsWork') }}</a>
</div>
<div
v-if="multipleApprovalsRequested && userIsManager"
class="claim-bottom-message d-flex align-items-center"
>
<a @click="showRequests()">{{ $t('viewRequests') }}</a>
</div>
<div
v-if="userIsAssigned && task.group.approval.approved
&& !task.completed && task.type !== 'habit'"
class="claim-bottom-message d-flex align-items-center justify-content-around"
>
<a
class="approve-color"
@click="$emit('claimRewards')"
>{{ $t('claimRewards') }}</a>
</div>
</div>
</template>
@@ -61,148 +119,220 @@
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.claim-bottom-message {
background-color: $gray-700;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
color: $gray-200;
background-color: $gray-600;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
font-size: 12px;
line-height: 1.334;
padding-bottom: 0.25rem;
padding-top: 0.25rem;
z-index: 9;
height: 24px;
.blue-10 {
color: $blue-10;
}
}
.approve-color {
color: $green-10 !important;
.completion-row {
height: 3rem;
background-color: $gray-700;
font-size: 12px;
.control {
background-color: $gray-200;
border-top: 1px solid $gray-100;
width: 40px;
height: 48px;
padding: 9px 6px;
.inner {
width: 28px;
height: 28px;
padding: 6px;
border-radius: 2px;
background-color: rgba($white, 0.5);
cursor: default;
&.interactive {
cursor: pointer;
&:hover {
background-color: rgba($white, 0.75);
}
}
.icon {
color: $gray-10;
&.lock {
width: 10px;
}
&.check {
margin-left: 2px;
width: 12px;
}
}
}
}
.info {
width: 100%;
border-top: 1px solid $gray-600;
}
}
.claim-color {
color: $blue-10 !important;
.gray-100 {
color: $gray-100;
}
.unclaim-color {
color: $red-50 !important;
.green-10 {
color: $green-10;
}
.last-completed {
width: 16px;
height: 14px;
margin-bottom: 4px;
}
.small-check {
display: inline-flex;
width: 16px;
height: 16px;
border-radius: 2px;
background-color: $gray-500;
.svg-icon {
width: 8px;
height: 6px;
margin: auto;
}
}
.users-icon {
width: 16px;
}
</style>
<script>
import findIndex from 'lodash/findIndex';
import moment from 'moment';
import reduce from 'lodash/reduce';
import { mapState } from '@/libs/store';
import approvalModal from './approvalModal';
import sync from '@/mixins/sync';
import checkIcon from '@/assets/svg/check.svg';
import lockIcon from '@/assets/svg/lock.svg';
import usersIcon from '@/assets/svg/users.svg';
import lastComplete from '@/assets/svg/last-complete.svg';
export default {
components: {
approvalModal,
},
mixins: [sync],
props: ['task', 'group'],
data () {
return {
showStatus: false,
icons: Object.freeze({
check: checkIcon,
lastComplete,
lock: lockIcon,
users: usersIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
userIsAssigned () {
return this.task.group.assignedUsers
&& this.task.group.assignedUsers.indexOf(this.user._id) !== -1;
},
message () {
const { assignedUsers } = this.task.group;
const assignedUsersNames = [];
const assignedUsersLength = assignedUsers.length;
// @TODO: Eh, I think we only ever display one user name
if (this.group && this.group.members) {
assignedUsers.forEach(userId => {
const index = findIndex(this.group.members, member => member._id === userId);
const assignedMember = this.group.members[index];
assignedUsersNames.push(`@${assignedMember.auth.local.username}`);
});
}
if (assignedUsersLength === 1 && !this.userIsAssigned) {
return this.$t('assignedToUser', { userName: assignedUsersNames[0] });
} if (assignedUsersLength > 1 && !this.userIsAssigned) {
return this.$t('assignedToMembers', { userCount: assignedUsersLength });
} if (assignedUsersLength > 1 && this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: assignedUsersLength - 1 });
} if (this.userIsAssigned) {
return this.$t('youAreAssigned');
} // if (assignedUsersLength === 0) {
return this.$t('taskIsUnassigned');
return this.task.group.assignedUsersDetail
&& Boolean(this.task.group.assignedUsersDetail[this.user._id]);
},
userIsManager () {
if (
this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id])
) return true;
return false;
return this.group
&& (this.group.leader.id === this.user._id || this.group.managers[this.user._id]);
},
approvalRequested () {
if (
(this.task.approvals && this.task.approvals.length === 1)
|| (this.task.group && this.task.group.approval && this.task.group.approval.requested)
) {
return true;
assignedUsersCount () {
return this.task.group.assignedUsers.length;
},
completionsCount () {
return reduce(this.task.group.assignedUsersDetail, (count, assignment) => {
if (assignment.completed) return count + 1;
return count;
}, 0);
},
completionsList () {
if (this.assignedUsersCount === 1) return [];
const completionsArray = [];
for (const userId of this.task.group.assignedUsers) {
if (userId !== this.user._id) {
const { completedDate } = this.task.group.assignedUsersDetail[userId];
let completedDateString;
if (moment().diff(completedDate, 'days') > 0) {
completedDateString = `Completed ${moment(completedDate).format('M/D/YY')}`;
} else {
completedDateString = `Completed at ${moment(completedDate).format('h:mm A')}`;
}
completionsArray.push({
userId,
userName: this.task.group.assignedUsersDetail[userId].assignedUsername,
completed: this.task.group.assignedUsersDetail[userId].completed,
completedDate,
completedDateString,
});
}
}
return false;
return completionsArray;
},
multipleApprovalsRequested () {
if (this.task.approvals && this.task.approvals.length > 1) return true;
return false;
message () {
if (this.assignedUsersCount > 1) { // Multi assigned
if (this.userIsAssigned) {
return this.$t('assignedToYouAndMembers', { userCount: this.assignedUsersCount - 1 });
}
return this.$t('assignedToMembers', { userCount: this.assignedUsersCount });
}
if (this.assignedUsersCount === 1) { // Singly assigned
const userId = this.task.group.assignedUsers[0];
const userName = this.task.group.assignedUsersDetail[userId].assignedUsername;
if (this.task.group.assignedUsersDetail[userId].completed) { // completed
const { completedDate } = this.task.group.assignedUsersDetail[userId];
if (this.userIsAssigned) {
if (moment().diff(completedDate, 'days') > 0) {
return `<strong>You</strong> completed ${moment(completedDate).format('M/D/YY')}`;
}
return `<strong>You</strong> completed at ${moment(completedDate).format('h:mm A')}`;
}
if (moment().diff(completedDate, 'days') > 0) {
return `@${userName} completed ${moment(completedDate).format('M/D/YY')}`;
}
return `@${userName} completed at ${moment(completedDate).format('h:mm A')}`;
}
if (this.userIsAssigned) {
return this.$t('youEmphasized');
}
return `@${userName}`;
}
return this.$t('error'); // task is open, or the other conditions aren't hitting right
},
singleAssignLastDone () {
const completion = this.task?.group?.assignedUsersDetail[this.user._id];
if (completion) return completion.completedDate;
return null;
},
formattedCompletionTime () {
if (!this.singleAssignLastDone) return '';
if (moment().diff(this.singleAssignLastDone, 'days') < 1) {
return moment(this.singleAssignLastDone).format('h:mm A');
}
return moment(this.singleAssignLastDone).format('M/D/YY');
},
},
methods: {
async claim () {
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:assignTask', {
taskId,
userId: this.user._id,
});
this.task.group.assignedUsers.push(this.user._id);
this.sync();
iconClass (completion) {
if (this.userIsManager && completion.completed) return 'check';
return 'lock';
},
async unassign () {
if (!window.confirm(this.$t('confirmUnClaim'))) return; // eslint-disable-line no-alert
let taskId = this.task._id;
// If we are on the user task
if (this.task.userId) {
taskId = this.task.group.taskId;
}
await this.$store.dispatch('tasks:unassignTask', {
taskId,
userId: this.user._id,
});
const index = this.task.group.assignedUsers.indexOf(this.user._id);
this.task.group.assignedUsers.splice(index, 1);
this.sync();
},
approve () {
const userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(0, 1);
this.task.approvals.splice(0, 1);
this.sync();
},
needsWork () {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[0];
needsWork (completion) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
userId: completion.userId,
});
this.task.approvals.splice(0, 1);
this.sync();
},
showRequests () {
this.$root.$emit('bv::show::modal', 'approval-modal');
this.task.group.assignedUsersDetail[completion.userId].completed = false;
},
},
};
@@ -1,80 +0,0 @@
<template>
<b-modal
id="approval-modal"
:title="$t('approveTask')"
size="md"
:hide-footer="true"
>
<div class="modal-body">
<!-- eslint-disable-next-line vue/require-v-for-key -->
<div
v-for="(approval, index) in task.approvals"
class="row approval"
>
<div class="col-8">
<strong>{{ approval.userId.profile.name }}</strong>
</div>
<div class="col-2">
<button
class="btn btn-primary"
@click="approve(index)"
>
{{ $t('approve') }}
</button>
</div>
<div class="col-2">
<button
class="btn btn-secondary"
@click="needsWork(index)"
>
{{ $t('needsWork') }}
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('close') }}
</button>
</div>
</b-modal>
</template>
<style scoped>
.row.approval {
padding-top: 1em;
padding-bottom: 1em;
}
</style>
<script>
export default {
props: ['task'],
methods: {
approve (index) {
const userIdToApprove = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:approve', {
taskId: this.task._id,
userId: userIdToApprove,
});
this.task.group.assignedUsers.splice(index, 1);
this.task.approvals.splice(index, 1);
},
needsWork (index) {
if (!window.confirm(this.$t('confirmNeedsWork'))) return; // eslint-disable-line no-alert
const userIdNeedsMoreWork = this.task.group.assignedUsers[index];
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: userIdNeedsMoreWork,
});
this.task.approvals.splice(index, 1);
},
close () {
this.$root.$emit('bv::hide::modal', 'approval-modal');
},
},
};
</script>
+38 -15
View File
@@ -43,7 +43,7 @@
class="tasks-list"
>
<textarea
v-if="isUser"
v-if="isUser || canCreateTasks()"
ref="quickAdd"
v-model="quickAddText"
class="quick-add"
@@ -102,6 +102,7 @@
:group="group"
:challenge="challenge"
@editTask="editTask"
@taskSummary="taskSummary"
@moveTo="moveTo"
@taskDestroyed="taskDestroyed"
/>
@@ -347,18 +348,19 @@
import throttle from 'lodash/throttle';
import isEmpty from 'lodash/isEmpty';
import draggable from 'vuedraggable';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import Task from './task';
import ClearCompletedTodos from './clearCompletedTodos';
import buyMixin from '@/mixins/buy';
import sync from '@/mixins/sync';
import { mapState, mapActions, mapGetters } from '@/libs/store';
import shopItem from '../shops/shopItem';
import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue';
import PinBadge from '@/components/ui/pinBadge';
import notifications from '@/mixins/notifications';
import { shouldDo } from '@/../../common/script/cron';
import inAppRewards from '@/../../common/script/libs/inAppRewards';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import {
getTypeLabel,
@@ -382,7 +384,7 @@ export default {
shopItem,
draggable,
},
mixins: [buyMixin, notifications],
mixins: [buyMixin, notifications, sync],
// @TODO Set default values for props
// allows for better control of props values
// allows for better control of where this component is called
@@ -542,6 +544,7 @@ export default {
...mapActions({
loadCompletedTodos: 'tasks:fetchCompletedTodos',
createTask: 'tasks:create',
createGroupTasks: 'tasks:createGroupTasks',
}),
async taskSorted (data) {
const filteredList = this.taskList;
@@ -574,18 +577,25 @@ export default {
},
async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id;
const list = this.getUnfilteredTaskList(this.type);
const list = this.taskListOverride || this.getUnfilteredTaskList(this.type);
const oldPosition = list.findIndex(t => t._id === taskIdToMove);
const moved = list.splice(oldPosition, 1);
const newPosition = where === 'top' ? 0 : list.length;
list.splice(newPosition, 0, moved[0]);
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
if (!this.isUser) {
await this.$store.dispatch('tasks:moveGroupTask', {
taskId: taskIdToMove,
position: newPosition,
});
} else {
const newOrder = await this.$store.dispatch('tasks:move', {
taskId: taskIdToMove,
position: newPosition,
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
}
},
async rewardSorted (data) {
const rewardsList = this.inAppRewards;
@@ -606,7 +616,12 @@ export default {
this.showPopovers = true;
this.isDragging(false);
},
quickAdd (ev) {
canCreateTasks () {
if (!this.group) return false;
return (this.group.leader && this.group.leader._id === this.user._id)
|| (this.group.managers && Boolean(this.group.managers[this.user._id]));
},
async quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
if (ev.shiftKey) {
this.quickAddRows += 1;
@@ -620,19 +635,27 @@ export default {
const tasks = text.split('\n').reverse().filter(taskText => (!!taskText)).map(taskText => {
const task = taskDefaults({ type: this.type, text: taskText }, this.user);
task.tags = this.selectedTags.slice();
if (this.isUser) task.tags = this.selectedTags.slice();
return task;
});
this.quickAddText = '';
this.quickAddRows = 1;
this.createTask(tasks);
if (this.group) {
await this.createGroupTasks({ groupId: this.group.id, tasks });
this.sync();
} else {
this.createTask(tasks);
}
this.$refs.quickAdd.blur();
return true;
},
editTask (task) {
this.$emit('editTask', task);
},
taskSummary (task) {
this.$emit('taskSummary', task);
},
activateFilter (type, filter = '') {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
@@ -687,7 +710,7 @@ export default {
filterByLabel (taskList, type, filter) {
if (!taskList) return [];
const selectedFilter = getActiveFilter(type, filter, this.challenge);
return sortAndFilterTasks(taskList, selectedFilter);
return sortAndFilterTasks(taskList, selectedFilter, Boolean(this.group));
},
filterByTagList (taskList, tagList = []) {
let filteredTaskList = taskList;
@@ -1,80 +1,98 @@
<template>
<div class="checklist-component">
<lockable-label
:locked="disabled || disableItems"
:text="$t('checklist')"
/>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
<div
class="d-flex"
>
<lockable-label
:locked="disabled"
:text="$t('checklist')"
/>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
class="svg-icon icon-16 my-auto ml-auto pointer"
:class="{'chevron-flip': showChecklist}"
v-html="icons.chevron"
@click="showChecklist = !showChecklist"
>
</div>
</div>
<b-collapse
id="checklistCollapse"
v-model="showChecklist"
>
<draggable
v-model="checklist"
:options="{
handle: '.grippy',
filter: '.task-dropdown',
disabled: disabled,
}"
@update="updateChecklist"
>
<div
v-for="(item, $index) in checklist"
:key="item.id"
class="inline-edit-input-group checklist-group input-group"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
>
</span>
<checkbox
v-if="!disableEdit"
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled"
class="input-group-prepend"
:class="{'cursor-auto': disabled}"
/>
<input
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableEdit"
:class="summaryClass(item)"
>
<span
v-if="!disabled && !disableEdit"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableEdit"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-if="!disabled && !disableDrag"
class="grippy"
v-html="icons.grip"
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<checkbox
:id="`checklist-${item.id}`"
:checked.sync="item.completed"
:disabled="disabled || disableItems"
class="input-group-prepend"
:class="{'cursor-auto': disabled || disableItems}"
/>
<input
v-model="item.text"
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableItems"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
<span
v-if="!disabled && !disableItems"
class="input-group-append"
@click="removeChecklistItem($index)"
>
<div
v-once
class="svg-icon destroy-icon"
v-html="icons.destroy"
>
</div>
</span>
</div>
</draggable>
<div
v-if="!disabled && !disableItems"
class="inline-edit-input-group checklist-group input-group new-checklist"
:class="{'top-border': items.length === 0}"
>
<span
v-once
class="input-group-prepend new-icon"
v-html="icons.positive"
>
</span>
<input
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@keyup.enter="addChecklistItem($event, true)"
@blur="addChecklistItem($event, false)"
>
</div>
</b-collapse>
</div>
</template>
@@ -104,7 +122,7 @@ export default {
disableDrag: {
type: Boolean,
},
disableItems: {
disableEdit: {
type: Boolean,
},
items: {
@@ -114,6 +132,7 @@ export default {
data () {
return {
checklist: this.items,
showChecklist: true,
hasPossibilityOfIMEConversion: true,
newChecklistItem: null,
icons: Object.freeze({
@@ -125,6 +144,11 @@ export default {
};
},
methods: {
summaryClass (item) {
if (!this.disableEdit) return '';
if (item.completed) return 'summary-completed';
return 'summary-incomplete';
},
updateChecklist () {
this.$emit('update:items', this.checklist);
},
@@ -166,14 +190,22 @@ export default {
.checklist-component {
.top-border {
border-top: 1px solid $gray-500;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
}
.lock-icon {
color: $gray-200;
}
.pointer {
cursor: pointer;
}
.top-border {
border-top: 1px solid $gray-500;
}
.checklist-group {
height: 2rem;
border-bottom: 1px solid $gray-500;
@@ -251,6 +283,13 @@ export default {
border-radius: 0px;
border: none !important;
padding-left: 36px;
&.summary-incomplete {
opacity: 1;
}
&.summary-completed {
text-decoration: line-through;
}
}
.checklist-group {
@@ -4,16 +4,16 @@
:class="{ 'break': maxItems === 0 }"
>
<template v-if="items.length === 0">
<div class="items-none">
<div class="items-none mb-1">
{{ emptyMessage }}
</div>
</template>
<template v-else>
<div
v-for="item in truncatedSelectedItems"
v-for="item in items"
:key="item.id"
:title="item.name"
class="multi-item mr-1 d-inline-flex align-items-center"
class="multi-item mr-1 mb-1 d-inline-flex align-items-center"
:class="{'margin-adjust': maxItems !== 0, 'pill-invert': pillInvert}"
@click.stop="removeItem($event, item)"
@@ -27,12 +27,6 @@
v-html="icons.remove"
></div>
</div>
<div
v-if="remainingSelectedItems.length > 0"
class="items-more ml-75"
>
+{{ remainingSelectedItems.length }}
</div>
</template>
</div>
</template>
@@ -1,4 +1,4 @@
multi<template>
<template>
<div>
<b-dropdown
ref="dropdown"
@@ -29,6 +29,7 @@ multi<template>
</b-dropdown-header>
<template v-slot:button-content>
<multi-list
class="d-flex flex-wrap"
:items="selectedItemsAsObjects"
:add-new="addNew"
:pill-invert="pillInvert"
@@ -85,6 +86,13 @@ multi<template>
$itemHeight: 2rem;
.inline-dropdown {
&.select-multi .dropdown-toggle {
height: auto;
padding-bottom: 0px;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
@@ -0,0 +1,294 @@
<template>
<div>
<b-dropdown
ref="dropdown"
class="inline-dropdown select-multi"
:toggle-class="isOpened ? 'active' : null"
:class="{'margin-adjust': selectedItem}"
@show="wasOpened()"
@hide="hideCallback($event)"
@toggle="openOrClose($event)"
>
<b-dropdown-header>
<div class="mb-2">
<b-form-input
v-model="search"
type="text"
:placeholder="searchPlaceholder"
@keyup.enter="handleSubmit"
/>
</div>
</b-dropdown-header>
<template v-slot:button-content>
<div
class="mr-1 d-inline-flex align-items-center"
@click.stop="selectItem({id: selectedItem})"
v-markdown="
allItemsMap[selectedItem] ? `@${allItemsMap[selectedItem].name}`
: emptyMessage
"
>
</div>
</template>
<div
v-if="addNew || availableToSelect.length > 0"
:class="{
'item-group': true,
'add-new': availableToSelect.length === 0 && search !== '',
'scroll': availableToSelect.length > 5
}"
>
<b-dropdown-item-button
v-for="item in availableToSelect"
:key="item.id"
class="ignore-hide multi-item"
:class="{ 'none': item.id === 'none', selectListItem: true }"
@click.prevent.stop="selectItem(item)"
>
<div
v-markdown="item.name"
class="label"
></div>
<div
v-if="item.addlText"
class="addl-text"
>
{{ item.addlText }}
</div>
</b-dropdown-item-button>
</div>
</b-dropdown>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
$itemHeight: 2rem;
.selected-item {
display: inline-block;
height: 1.5rem;
border-radius: 100px;
background-color: $white;
border: solid 1px $gray-400;
position: relative;
top: -1px;
.multi-label {
height: 1rem;
font-size: 12px;
line-height: 16px;
letter-spacing: normal;
color: $gray-100;
}
}
.select-multi {
.dropdown-toggle {
padding-left: 0.75rem;
}
.dropdown-header {
background-color: $gray-700;
padding-bottom: 0;
min-height: 3rem;
}
.dropdown-item, .dropdown-header {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.none {
cursor: default;
pointer-events: none;
}
.multi-item button {
height: $itemHeight;
display: flex;
.label {
height: 1.5rem;
font-size: 14px;
line-height: 1.71;
flex: 1;
}
.addl-text {
height: 1rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
text-align: right;
color: $gray-100;
align-self: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
margin-right: 0.25rem;
}
&:hover {
.addl-text {
color: $purple-300;
}
}
}
.item-group {
max-height: #{5*$itemHeight};
&.add-new {
height: 30px;
.hint {
display: block;
}
}
&.scroll {
overflow-y: scroll;
}
}
.hint {
display: none;
height: 2rem;
font-size: 12px;
font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 1.33;
letter-spacing: normal;
color: $gray-100;
margin-left: 0.75rem;
margin-top: 0.5rem;
}
}
</style>
<script>
import Vue from 'vue';
import markdownDirective from '@/directives/markdown';
export default {
directives: {
markdown: markdownDirective,
},
components: {
},
props: {
addNew: {
type: Boolean,
default: false,
},
allItems: {
type: Array,
},
emptyMessage: {
type: String,
},
searchPlaceholder: {
type: String,
},
selectedItem: {
type: String,
},
},
data () {
return {
preventHide: true,
isOpened: false,
selected: this.selectedItem,
search: '',
};
},
computed: {
allItemsMap () {
const obj = {};
this.allItems.forEach(t => {
obj[t.id] = t;
});
return obj;
},
selectedItemAsObject () {
return this.selectedItem ? this.allItemsMap[this.selectedItem] : null;
},
availableToSelect () {
const searchString = this.search.toLowerCase();
const filteredItems = this.allItems.filter(i => i.name.toLowerCase().includes(searchString));
return filteredItems;
},
},
watch: {
selected () {
this.$emit('changed', this.selectedItem);
},
},
created () {
document.addEventListener('keyup', this.handleEsc);
},
beforeDestroy () {
document.removeEventListener('keyup', this.handleEsc);
},
mounted () {
this.$refs.dropdown.clickOutHandler = () => {
this.closeSelectPopup();
};
},
methods: {
closeSelectPopup () {
this.preventHide = false;
this.isOpened = false;
Vue.nextTick(() => {
this.$refs.dropdown.hide();
});
},
openOrClose ($event) {
if (this.isOpened) {
this.closeSelectPopup();
$event.preventDefault();
}
},
selectItem (item) {
if (item.id === this.selectedItem) {
this.$emit('toggle', null);
} else {
this.$emit('toggle', item.id);
}
this.closeSelectPopup();
},
hideCallback ($event) {
if (this.preventHide) {
$event.preventDefault();
return;
}
this.isOpened = false;
},
wasOpened () {
this.isOpened = true;
this.preventHide = true;
},
handleEsc (e) {
if (e.keyCode === 27) {
this.closeSelectPopup();
}
},
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
this.$emit('addNew', search);
this.search = '';
},
},
};
</script>
+15 -17
View File
@@ -68,7 +68,8 @@
</b-popover>
<div
class="spell-border"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl }"
:class="{ disabled: spellDisabled(key) || user.stats.lvl < skill.lvl,
'insufficient-mana': user.stats.mp < skill.mana }"
>
<div
class="spell"
@@ -87,19 +88,6 @@
<div>Level {{ skill.lvl }}</div>
</div>
</div>
<div
v-else-if="spellDisabled(key) === true"
class="mana"
>
<div class="mana-text">
<div
v-once
class="svg-icon"
v-html="icons.mana"
></div>
<div>{{ skill.mana }}</div>
</div>
</div>
<div
v-else
class="mana"
@@ -200,7 +188,7 @@
border-radius: 4px;
margin-bottom: 1rem;
&:hover:not(.disabled) {
&:hover:not(.disabled):not(.insufficient-mana) {
background-color: $purple-400;
cursor: pointer;
box-shadow: 0 4px 4px 0 rgba(26, 24, 29, 0.16),
@@ -216,6 +204,10 @@
background-color: rgba(26, 24, 29, 0.5);
}
.mana-text {
color: $blue-500;
}
.level {
color: $white;
font-weight: normal;
@@ -223,6 +215,10 @@
}
}
&.insufficient-mana:not(.disabled) {
opacity: 0.5;
}
.spell {
background: $white;
border-radius: 4px;
@@ -465,7 +461,7 @@ export default {
spellDisabled (skill) {
const incompleteDailiesDue = this
.getUnfilteredTaskList('daily')
.filter(daily => !daily.completed && daily.isDue)
.filter(daily => !daily.completed && !daily.group.id && daily.isDue)
.length;
if (skill === 'frost' && this.user.stats.buffs.streaks) {
@@ -481,7 +477,9 @@ export default {
skillNotes (skill) {
let notes = skill.notes();
if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
if (this.user.stats.lvl < skill.lvl) {
notes = this.$t('spellLevelTooHigh', { level: skill.lvl });
} else if (skill.key === 'frost' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
} else if (skill.key === 'stealth' && this.spellDisabled(skill.key)) {
notes = this.$t('spellAlreadyCast');
+139 -84
View File
@@ -3,47 +3,39 @@
<div
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess},
`type_${task.type}`
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
>
<approval-header
v-if="task.group.id"
:task="task"
:group="group"
/>
<div
class="d-flex"
:class="{'task-not-scoreable': isUser !== true || task.group.approval.requested
&& !(task.group.approval.approved && task.type === 'habit')}"
:class="{'task-not-scoreable': showTaskLockIcon }"
>
<!-- Habits left side control-->
<div
v-if="task.type === 'habit'"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass
}, controlClass.up.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-positive-enabled': task.up && isUser,
'habit-control-positive-disabled': !task.up && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.up.inner]"
tabindex="0"
@click="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('up') : null"
@click="score('up')"
@keypress.enter="score('up')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.up ? controlClass.up.icon : 'positive'"
v-html="icons.lock"
@@ -60,20 +52,21 @@
v-if="task.type === 'daily' || task.type === 'todo'"
class="left-control d-flex justify-content-center"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.bg]"
>
<div
class="task-control daily-todo-control"
:class="controlClass.inner"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.inner,
]"
tabindex="0"
@click="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@keypress.enter="isUser && !task.group.approval.requested
? score(task.completed ? 'down' : 'up' ) : null"
@click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="controlClass.icon"
v-html="icons.lock"
@@ -82,7 +75,7 @@
v-else
class="svg-icon check"
:class="{
'display-check-icon': task.completed || task.group.approval.requested,
'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
@@ -95,8 +88,8 @@
:class="contentClass"
>
<div
class="task-clickable-area"
:class="{ 'cursor-auto': !isUser && !teamManagerAccess }"
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
@@ -105,14 +98,14 @@
<h3
v-markdown="task.text"
class="task-title markdown"
:class="{ 'has-notes': task.notes || (!isUser && task.group.managerNotes)}"
:class="{ 'has-notes': task.notes }"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
ref="taskDropdown"
v-b-tooltip.hover.top="$t('options')"
tabindex="0"
class="task-dropdown"
class="task-dropdown mr-1"
:right="task.type === 'reward'"
>
<div slot="dropdown-toggle">
@@ -138,7 +131,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToTop"
@@ -153,7 +145,6 @@
</span>
</div>
<div
v-if="isUser"
class="dropdown-item"
tabindex="0"
@click="moveToBottom"
@@ -186,7 +177,7 @@
</menu-dropdown>
</div>
<div
v-markdown="displayNotes"
v-markdown="task.notes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
@@ -220,7 +211,7 @@
v-if="!task.collapseChecklist"
:key="item.id"
class="custom-control custom-checkbox checklist-item"
:class="{'checklist-item-done': item.completed, 'cursor-auto': !isUser}"
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<input
@@ -229,14 +220,14 @@
tabindex="0"
type="checkbox"
:checked="item.completed"
:disabled="castingSpell || !isUser"
:disabled="castingSpell || showTaskLockIcon"
@change="toggleChecklistItem(item)"
@keypress.enter="toggleChecklistItem(item)"
>
<label
v-markdown="item.text"
class="custom-control-label"
:class="{ 'cursor-auto': !isUser }"
:class="{ 'cursor-auto': showTaskLockIcon }"
:for="`checklist-${item.id}-${random}`"
></label>
</div>
@@ -316,7 +307,7 @@
></div>
</div>
<div
v-if="hasTags"
v-if="hasTags && !task.group.id"
:id="`tags-icon-${task._id}`"
class="d-flex align-items-center"
>
@@ -326,7 +317,7 @@
></div>
</div>
<b-popover
v-if="hasTags"
v-if="hasTags && !task.group.id"
:target="`tags-icon-${task._id}`"
triggers="hover"
placement="bottom"
@@ -356,25 +347,22 @@
v-if="task.type === 'habit'"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id,
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.down.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-negative-enabled': task.down && isUser,
'habit-control-negative-disabled': !task.down && isUser,
'task-not-scoreable': isUser !== true
|| (task.group.approval.requested && !task.group.approval.approved),
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.down.inner]"
tabindex="0"
@click="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|| task.group.approval.approved)) ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="!isUser"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.down ? controlClass.down.icon : 'negative'"
v-html="icons.lock"
@@ -390,13 +378,22 @@
<div
v-if="task.type === 'reward'"
class="right-control d-flex align-items-center justify-content-center reward-control"
:class="controlClass.bg"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.bg,
]"
tabindex="0"
@click="isUser ? score('down') : null"
@keypress.enter="isUser ? score('down') : null"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
class="svg-icon"
v-if="showTaskLockIcon"
class="svg-icon color lock"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon mb-1"
v-html="icons.gold"
></div>
<div class="small-text">
@@ -405,10 +402,9 @@
</div>
</div>
<approval-footer
v-if="task.group.id"
v-if="task.group.id && !isOpenTask"
:task="task"
:group="group"
@claimRewards="score('up')"
/>
</div>
</div>
@@ -427,7 +423,7 @@
border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 2px;
border-radius: 4px;
}
}
@@ -449,11 +445,11 @@
margin-bottom: 2px;
box-shadow: 0 2px 2px 0 rgba($black, 0.16), 0 1px 4px 0 rgba($black, 0.12);
background: white;
border-radius: 2px;
border-radius: 4px;
position: relative;
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
}
@@ -469,10 +465,10 @@
}
.task.groupTask {
&:hover:not(.task-not-editable),
&:focus-within:not(.task-not-editable) {
&:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within:not(.task-not-editable.task-not-scoreable) {
border: $purple-400 solid 1px;
border-radius: 3px;
border-radius: 5px;
margin: -1px; // to counter the border width
margin-bottom: 1px;
transition: none; // with transition, the border color switches from black to $purple-400
@@ -495,6 +491,7 @@
}
&.has-notes {
margin-bottom: 0px;
padding-bottom: 4px;
}
@@ -508,8 +505,6 @@
}
.task-clickable-area {
padding: 7px 8px;
padding-bottom: 0px;
border: transparent solid 1px;
cursor: pointer;
@@ -518,7 +513,7 @@
}
&:focus {
border-radius: 2px;
border-radius: 4px;
border: $purple-400 solid 1px;
}
}
@@ -575,7 +570,11 @@
}
.task-dropdown {
max-height: 18px;
height: 16px;
width: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.task-dropdown ::v-deep .dropdown-menu {
@@ -632,8 +631,8 @@
}
&.reward-content {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
}
@@ -796,8 +795,8 @@
}
}
.left-control {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
min-height: 60px;
border: 1px solid transparent;
border-right: none;
@@ -809,15 +808,15 @@
.task:not(.type_habit) {
.left-control {
& + .task-content {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
}
}
.right-control {
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
min-height: 56px;
border: 1px solid transparent;
border-left: none;
@@ -853,8 +852,12 @@
height: 24px;
}
.lock {
color: $gray-200;
width: 15px;
}
.small-text {
margin-top: 4px;
font-style: initial;
font-weight: bold;
}
@@ -920,20 +923,19 @@ import lockIcon from '@/assets/svg/lock.svg';
import menuIcon from '@/assets/svg/menu.svg';
import markdownDirective from '@/directives/markdown';
import scoreTask from '@/mixins/scoreTask';
import approvalHeader from './approvalHeader';
import sync from '@/mixins/sync';
import approvalFooter from './approvalFooter';
import MenuDropdown from '../ui/customMenuDropdown';
export default {
components: {
approvalFooter,
approvalHeader,
MenuDropdown,
},
directives: {
markdown: markdownDirective,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
// @TODO: maybe we should store the group on state?
props: {
task: {},
@@ -1064,11 +1066,45 @@ export default {
},
teamManagerAccess () {
if (!this.isGroupTask || !this.group) return true;
if (!this.group.leader && !this.group.managers) return false;
return (this.group.leader._id === this.user._id || this.group.managers[this.user._id]);
},
displayNotes () {
if (this.isGroupTask && !this.isUser) return this.task.group.managerNotes;
return this.task.notes;
isOpenTask () {
if (!this.isGroupTask) return false;
if (this.task?.group?.assignedUsers?.length > 0) return false;
return true;
},
showCheckIcon () {
if (this.isGroupTask && this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]) {
return this.task.group.assignedUsersDetail[this.user._id].completed;
}
return this.task.completed;
},
showTaskLockIcon () {
if (this.isUser) return false;
if (this.isGroupTask) {
if (this.task.completed) {
if (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]
) {
return false;
}
if (this.task.group.completedBy.userId === this.user._id) return false;
if (this.teamManagerAccess) {
if (!this.task.group.assignedUsers || this.task.group.assignedUsers.length === 0) {
return false;
}
if (this.task.group.assignedUsers.length === 1) return false;
}
return true;
}
if (this.isOpenTask) return false;
if (this.task.group.assignedUsersDetail[this.user._id]) {
return false;
}
}
return true;
},
},
methods: {
@@ -1098,7 +1134,7 @@ export default {
return this.task.date && this.$t('dueIn', { dueIn });
},
edit (e, task) {
if (this.isRunningYesterdailies || !this.showEdit) return;
if (this.isRunningYesterdailies) return;
const target = e.target || e.srcElement;
/*
@@ -1116,8 +1152,11 @@ export default {
const isEditAction = this.$refs.editTaskItem && this.$refs.editTaskItem.contains(target);
if (isDropdown && !isEditAction) return;
if (this.$store.state.spellOptions.castingSpell) return;
if (!this.$store.state.spellOptions.castingSpell) {
if (!this.showEdit) {
this.$emit('taskSummary', task);
} else {
this.$emit('editTask', task);
}
},
@@ -1137,6 +1176,22 @@ export default {
setTimeout(() => this.$root.$emit('castEnd', task, 'task', e), 0);
},
async score (direction) {
if (this.showTaskLockIcon) return;
if (this.task.type === 'habit' && !this.task[direction]) return;
if (
this.isGroupTask && direction === 'down'
&& ['todo', 'daily'].indexOf(this.task.type) !== -1
&& !((this.task.group.completedBy && this.task.group.completedBy.userId === this.user._id)
|| (this.task.group.assignedUsersDetail
&& this.task.group.assignedUsersDetail[this.user._id]))
) {
this.$store.dispatch('tasks:needsWork', {
taskId: this.task._id,
userId: this.task.group.assignedUsers[0] || this.task.group.completedBy.userId,
});
this.task.completed = false;
return;
}
if (this.isYesterdaily === true) {
await this.beforeTaskScore(this.task);
this.task.completed = !this.task.completed;
+76 -328
View File
@@ -6,7 +6,7 @@
size="sm"
:hide-footer="true"
@hidden="onClose()"
@show="handleOpen()"
@show="syncTask()"
@shown="focusInput()"
>
<div
@@ -22,7 +22,9 @@
>
{{ title }}
</h2>
<div class="ml-auto d-flex align-items-center">
<div
class="ml-auto d-flex align-items-center"
>
<button
class="cancel-task-btn mr-3"
:class="cssClass('headings')"
@@ -55,7 +57,7 @@
<div class="form-group">
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="`${$t('text')}*`"
/>
<input
@@ -66,28 +68,29 @@
type="text"
required="required"
spellcheck="true"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
>
</div>
<div
v-if="isUserTask || isChallengeTask || isOriginalChallengeTask"
class="form-group mb-0"
>
<label
class="d-flex align-items-center justify-content-between mb-1"
>
<span
:class="cssClass('headings')"
>{{ $t('notes') }}</span>
<small>
<div class="d-flex">
<lockable-label
class="mr-auto"
:class-override="cssClass('headings')"
:text="`${$t('notes')}`"
/>
<small
class="my-1"
>
<a
target="_blank"
href="https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet"
:class="cssClass('headings')"
>{{ $t('markdownHelpLink') }}</a>
</small>
</label>
</div>
<textarea
v-model="task.notes"
class="form-control input-notes"
@@ -95,49 +98,6 @@
:placeholder="$t('addNotes')"
></textarea>
</div>
<div
v-if="showManagerNotes"
class="form-group mb-0 mt-3"
>
<lockable-label
:class-override="cssClass('headings')"
:locked="groupAccessRequiredAndOnPersonalPage"
:text="$t('managerNotes')"
/>
<textarea
v-model="managerNotes"
class="form-control input-notes"
:class="cssClass('input')"
:placeholder="$t('addNotes')"
:disabled="groupAccessRequiredAndOnPersonalPage"
></textarea>
</div>
<div
v-if="task.group && task.group.assignedDate && !task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateOnly', {
date: formattedDate(task.group.assignedDate),
})"
>
</div>
<div
v-if="task.group && task.group.assignedDate && task.group.assigningUsername"
class="mt-3 mb-n2"
:class="cssClass('headings')"
v-html="$t('assignedDateAndUser', {
username: task.group.assigningUsername,
date: formattedDate(task.group.assignedDate),
})"
>
</div>
</div>
<div
v-if="task && groupAccessRequiredAndOnPersonalPage
&& (task.type === 'daily' || task.type === 'todo')"
class="summary-sentence py-3 px-4"
v-html="summarySentence"
>
</div>
<div
v-if="task"
@@ -181,8 +141,6 @@
>
<checklist
:items.sync="task.checklist"
:disable-items="groupAccessRequiredAndOnPersonalPage"
:disable-drag="groupAccessRequiredAndOnPersonalPage"
/>
</div>
<div
@@ -194,7 +152,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.up ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleUpDirection()"
>
<div
@@ -220,7 +178,7 @@
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
:class="!task.down ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
@click="toggleDownDirection()"
>
<div
@@ -243,11 +201,11 @@
</button>
</div>
<template
v-if="task.type !== 'reward' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type !== 'reward'"
>
<div class="d-flex mt-3">
<lockable-label
:locked="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:locked="challengeAccessRequired"
:text="$t('difficulty')"
/>
<div
@@ -258,13 +216,12 @@
</div>
<select-difficulty
:value="task.priority"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
:disabled="challengeAccessRequired"
@select="setDifficulty($event)"
/>
</template>
<div
v-if="task.type === 'todo' && !groupAccessRequiredAndOnPersonalPage
&& (!challengeAccessRequired || task.date)"
v-if="task.type === 'todo' && (!challengeAccessRequired || task.date)"
class="option mt-3"
>
<div class="form-group">
@@ -281,7 +238,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -297,7 +254,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -314,7 +271,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily'"
class="option mt-3"
>
<div class="form-group">
@@ -344,8 +301,7 @@
</div>
</div>
<div
v-if="task.type === 'daily' && task.frequency === 'weekly'
&& !groupAccessRequiredAndOnPersonalPage"
v-if="task.type === 'daily' && task.frequency === 'weekly'"
class="option mt-3"
>
<div class="form-group">
@@ -405,7 +361,7 @@
</div>
</div>
<div
v-if="isUserTask"
v-if="!groupId"
class="tags-select option mt-3"
>
<div class="tags-inline form-group row">
@@ -428,16 +384,16 @@
</div>
</div>
<div
v-if="task.type === 'habit'"
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
>
<div class="form-group">
<lockable-label
:locked="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:locked="challengeAccessRequired"
:text="$t('resetCounter')"
/>
<select-translated-array
:disabled="challengeAccessRequired || groupAccessRequiredAndOnPersonalPage"
:disabled="challengeAccessRequired"
:items="['daily', 'weekly', 'monthly']"
:value="task.frequency"
@select="task.frequency = $event"
@@ -448,25 +404,18 @@
v-if="groupId"
class="option group-options mt-3"
>
<div
v-if="task.type === 'todo'"
class="form-group"
>
<label
v-once
class="mb-1"
>{{ $t('sharedCompletion') }}</label>
<select-translated-array
:items="['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']"
:value="sharedCompletion"
@select="sharedCompletion = $event"
/>
</div>
<div class="form-group row mt-3 mb-3">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedTo') }}</label>
class="col-10 mb-1"
>{{ $t('assignTo') }}</label>
<a
v-if="assignedMembers.length > 0"
class="col-2 text-right mt-1"
@click="clearAssignments"
>
{{ $t('clear') }}
</a>
<div class="col-12">
<select-multi
ref="assignMembers"
@@ -479,17 +428,6 @@
/>
</div>
</div>
<div class="form-group flex-group mt-3 mb-4">
<label
v-once
class="mb-0 flex"
>{{ $t('approvalRequired') }}</label>
<toggle-switch
class="d-inline-block"
:checked="requiresApproval"
@change="updateRequiresApproval"
/>
</div>
</div>
<div
v-if="advancedSettingsAvailable"
@@ -605,9 +543,7 @@
</b-collapse>
</div>
<div
v-if="purpose !== 'create'
&& !challengeAccessRequired
&& !groupAccessRequiredAndOnPersonalPage"
v-if="purpose !== 'create' && !challengeAccessRequired"
class="d-flex justify-content-center mt-4 mb-4"
>
<button
@@ -654,6 +590,12 @@
@import '~@/assets/scss/colors.scss';
#task-modal {
a:not(.dropdown-item) {
font-size: 12px;
line-height: 1.33;
color: $blue-10;
}
.modal-dialog.modal-sm {
max-width: 448px;
}
@@ -686,9 +628,6 @@
}
input, textarea {
&:not(:host-context(.tags-popup)) {
border: none;
}
transition-property: border-color, box-shadow, color, background;
background-color: rgba(255, 255, 255, 0.5);
&:focus:not(:disabled), &:active:not(:disabled), &:hover:not(:disabled) {
@@ -1026,11 +965,6 @@
}
}
.summary-sentence {
background-color: $gray-700;
line-height: 1.71;
}
.input-group-text {
font-size: 14px;
font-weight: bold;
@@ -1051,12 +985,8 @@
<script>
import axios from 'axios';
import clone from 'lodash/clone';
import keys from 'lodash/keys';
import pickBy from 'lodash/pickBy';
import moment from 'moment';
import Datepicker from '@/components/ui/datepicker';
import toggleSwitch from '@/components/ui/toggleSwitch';
import toggleCheckbox from '@/components/ui/toggleCheckbox';
import markdownDirective from '@/directives/markdown';
import { mapGetters, mapActions, mapState } from '@/libs/store';
@@ -1066,6 +996,8 @@ import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
@@ -1076,11 +1008,11 @@ import chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg';
export default {
components: {
SelectMulti,
Datepicker,
toggleSwitch,
checklist,
selectDifficulty,
selectTranslatedArray,
@@ -1090,6 +1022,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [syncTask],
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {
@@ -1107,9 +1040,6 @@ export default {
calendar: calendarIcon,
grip: gripIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
sharedCompletion: 'singleCompletion',
managerNotes: '',
members: [],
membersNameAndId: [],
memberNamesById: {},
@@ -1123,15 +1053,6 @@ export default {
per: 'perception',
},
calendarHighlights: { dates: [new Date()] },
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
};
},
computed: {
@@ -1150,7 +1071,6 @@ export default {
|| this.task.type === 'todo'
|| this.purpose === 'create'
|| !this.isUserTask
|| this.groupAccessRequiredAndOnPersonalPage
) {
return false;
}
@@ -1164,29 +1084,17 @@ export default {
return true;
},
groupAccessRequiredAndOnPersonalPage () {
if (!this.groupId && this.task.group && this.task.group.id) return true;
return false;
},
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1
&& !this.isOriginalChallengeTask
&& (!this.groupAccessRequiredAndOnPersonalPage || this.checklist.length > 0);
},
showManagerNotes () {
return Boolean(this.task.group && this.task.group.managerNotes)
|| (
!this.groupAccessRequiredAndOnPersonalPage && this.managers.indexOf(this.user._id) !== -1
);
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
isChallengeTask () {
return Boolean(this.task.challenge && this.task.challenge.id);
},
onUserPage () {
isUserTask () {
return !this.challengeId && !this.groupId;
},
challengeAccessRequired () {
return this.onUserPage && this.isChallengeTask;
return this.isUserTask && this.isChallengeTask;
},
isOriginalChallengeTask () {
const isUserChallenge = Boolean(this.task.userId);
@@ -1203,9 +1111,6 @@ export default {
const type = this.$t(this.task.type);
return this.$t(this.purpose === 'edit' ? 'editATask' : 'createTask', { type });
},
isUserTask () {
return !this.challengeId && !this.groupId;
},
repeatSuffix () {
const { task } = this;
@@ -1240,23 +1145,6 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)}
task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
},
watch: {
task () {
@@ -1284,47 +1172,6 @@ export default {
createTask: 'tasks:create',
createTag: 'tags:createTag',
}),
async syncTask () {
if (this.task?.group?.managerNotes) {
this.managerNotes = this.task.group.managerNotes;
}
if (this.groupId && this.task.group?.approval) {
this.requiresApproval = this.task.group.approval.required;
}
if (this.task?.group?.sharedCompletion) {
this.sharedCompletion = this.task.group.sharedCompletion;
} else if (this.task.group) {
this.sharedCompletion = 'singleCompletion';
}
if (this.groupId) {
const members = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId,
includeAllPublicFields: true,
});
this.members = members;
this.membersNameAndId = [];
this.members.forEach(member => {
this.membersNameAndId.push({
id: member._id,
name: member.profile.name,
addlText: `@${member.auth.local.username}`,
});
this.memberNamesById[member._id] = member.profile.name;
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) {
this.assignedMembers = this.task.group.assignedUsers;
}
}
// @TODO: This whole component is mutating a prop
// and that causes issues. We need to not copy the prop similar to group modals
if (this.task) this.checklist = clone(this.task.checklist);
},
async handleOpen () {
this.syncTask();
},
cssClass (suffix) {
if (!this.task) {
return '';
@@ -1350,104 +1197,6 @@ export default {
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
calculateMonthlyRepeatDays (newRepeatsOn) {
if (!this.task) return;
const { task } = this;
@@ -1475,16 +1224,6 @@ export default {
if (!this.canSave) return;
if (this.newChecklistItem) this.addChecklistItem();
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
this.task.group.sharedCompletion = this.sharedCompletion;
this.task.managerNotes = this.managerNotes;
this.task.group.managerNotes = this.managerNotes;
}
if (this.task.type === 'reward' && this.task.value === '') {
this.task.value = 0;
}
@@ -1503,12 +1242,18 @@ export default {
tasks: [this.task],
});
Object.assign(this.task, response);
const promises = this.assignedMembers.map(memberId => this.$store.dispatch('tasks:assignTask', {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
}));
Promise.all(promises);
this.task.group.assignedUsers = this.assignedMembers;
assignedUserIds: this.assignedMembers,
});
this.assignedMembers.forEach(memberId => {
if (!this.task.assignedUsersDetail) this.task.assignedUsersDetail = {};
this.task.assignedUsersDetail[memberId] = {
assignedDate: new Date(),
assigningUsername: this.user.auth.local.username,
completed: false,
};
});
this.$emit('taskCreated', this.task);
} else {
this.createTask(this.task);
@@ -1530,22 +1275,14 @@ export default {
this.$root.$emit('bv::hide::modal', 'task-modal');
},
onClose () {
if (this.task.group && this.task.group.managerNotes) this.managerNotes = null;
this.newChecklistItem = '';
this.$emit('cancel');
},
updateRequiresApproval (newValue) {
let truthy = true;
if (!newValue) truthy = false; // This return undefined instad of false
this.requiresApproval = truthy;
},
async toggleAssignment (memberId) {
if (this.purpose === 'create') {
return;
}
const assignedIndex = this.assignedMembers.indexOf(memberId);
if (assignedIndex === -1) {
await this.$store.dispatch('tasks:unassignTask', {
taskId: this.task._id,
@@ -1554,10 +1291,21 @@ export default {
} else {
await this.$store.dispatch('tasks:assignTask', {
taskId: this.task._id,
userId: memberId,
assignedUserIds: [memberId],
});
}
},
async clearAssignments () {
if (this.purpose === 'edit') {
for (const assignedMember of this.assignedMembers) {
await this.$store.dispatch('tasks:unassignTask', { // eslint-disable-line no-await-in-loop
taskId: this.task._id,
userId: assignedMember,
});
}
}
this.assignedMembers = [];
},
focusInput () {
this.$refs.inputToFocus.focus();
},
@@ -0,0 +1,358 @@
<template>
<b-modal
id="task-summary"
:hide-footer="true"
@hidden="$emit('cancel')"
>
<div
v-if="task"
slot="modal-header"
class="task-modal-header px-4 d-flex align-items-center"
:class="cssClass('bg')"
>
<h2
class="my-auto"
:class="cssClass('headings')"
>
{{ title }}
</h2>
<div
class="svg-icon color close-x ml-auto my-auto"
:class="cssClass('headings')"
aria-hidden="true"
tabindex="0"
@click="cancel()"
@keypress.enter="cancel()"
v-html="icons.close"
></div>
</div>
<div
v-if="task"
class="task-modal-content pt-3 px-4 pb-4"
>
<div class="summary-block">
<h3> {{ $t('title') }} </h3>
<p> {{ task.text }} </p>
</div>
<div
v-if="task.notes"
class="summary-block"
>
<h3> {{ $t('notes') }} </h3>
<p> {{ task.notes }} </p>
</div>
<div class="summary-block">
<h3> {{ $t('description') }} </h3>
<p v-html="summarySentence" ></p>
</div>
<div
v-if="task.checklist && task.checklist.length > 0"
class="summary-block"
>
<checklist
:items.sync="task.checklist"
:disableDrag="true"
:disableEdit="true"
/>
</div>
<div
class="summary-block"
v-if="assignedUsernames.length > 0"
>
<h3> {{ $t('assignedTo') }} </h3>
<div
class="d-flex flex-wrap"
>
<span
v-for="member in assignedUsernames"
:key="member"
class="assigned-member py-1 px-75 mb-1 mr-1"
>
@{{ member }}
</span>
</div>
</div>
</div>
<div
v-if="task && task.group && task.group.assignedUsersDetail
&& task.group.assignedUsersDetail[user._id]"
class="assignment-footer text-center py-2"
v-html="$t('assignedDateAndUser', {
username: task.group.assignedUsersDetail[user._id].assigningUsername,
date: formattedDate(task.group.assignedUsersDetail[user._id].assignedDate),
})"
>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#task-summary {
overflow-y: hidden;
.modal-content {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border: none;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-header, .modal-body, .modal-footer {
padding: 0px;
border: none;
}
.modal-dialog {
width: 448px;
margin-top: 50vh;
transform: translateY(-50%);
}
.modal-header {
padding: 0px;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.assigned-member {
border: 1px solid $gray-400;
border-radius: 100px;
color: $gray-100;
font-size: 12px;
line-height: 1.33;
}
.assignment-footer {
color: $gray-100;
background-color: $gray-700;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.close-x {
height: 16px;
width: 16px;
position: relative;
opacity: 0.75;
cursor: pointer;
&:hover, &:focus {
opacity: 1;
}
}
.summary-block:not(:last-of-type) {
margin-bottom: 1rem;
}
.task-modal-content {
h3 {
margin-bottom: 0.25rem;
}
p {
margin-bottom: 0px;
}
h3, p {
color: $gray-50;
}
}
.task-modal-header {
color: $white;
height: 60px;
width: 100%;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
h2 {
color: $white;
}
}
</style>
<script>
import keys from 'lodash/keys';
import moment from 'moment';
import pickBy from 'lodash/pickBy';
import checklist from './modal-controls/checklist';
import { mapGetters, mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
export default {
components: {
checklist,
},
props: ['task'],
data () {
return {
expandDayString: {
su: 'Sunday',
m: 'Monday',
t: 'Tuesday',
w: 'Wednesday',
th: 'Thursday',
f: 'Friday',
s: 'Saturday',
},
icons: Object.freeze({
close: closeIcon,
}),
};
},
computed: {
...mapGetters({
getTaskClasses: 'tasks:getTaskClasses',
}),
...mapState({
user: 'user.data',
}),
assignedUsernames () {
if (!this.task.group || !this.task.group.assignedUsers
|| !this.task.group.assignedUsersDetail) return [];
const usernames = [];
for (const user of this.task.group.assignedUsers) {
usernames.push(this.task.group.assignedUsersDetail[user].assignedUsername);
}
return usernames;
},
summarySentence () {
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
}
if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
}
if (this.task.date) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
}
return `This is ${this.formattedDifficulty(this.task.priority)} task.`;
},
title () {
const type = this.$t(this.task.type);
return this.$t('taskSummary', { type });
},
},
methods: {
cancel () {
this.$root.$emit('bv::hide::modal', 'task-summary');
},
cssClass (suffix) {
if (!this.task) {
return '';
}
return this.getTaskClasses(this.task, `edit-modal-${suffix}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
},
formattedDays (frequency, repeat, daysOfMonth, weeksOfMonth, startDate) {
let activeDays;
const dayStringArray = [];
switch (frequency) {
case 'weekly':
activeDays = keys(pickBy(repeat, value => value === true));
if (activeDays.length === 0) return ' on <strong>no days</strong>';
if (activeDays.length === 7) return ' on <strong>every day of the week</strong>';
dayStringArray.push(' on <strong>');
activeDays.forEach((value, index) => {
if (activeDays.length > 1 && index === activeDays.length - 1) dayStringArray.push(' and');
dayStringArray.push(` ${this.expandDayString[value]}`);
if (activeDays.length > 2 && index !== activeDays.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
break;
case 'monthly':
dayStringArray.push(' on <strong>the ');
if (daysOfMonth.length > 0) {
daysOfMonth.forEach((value, index) => {
const stringDay = String(value);
const stringFinalDigit = stringDay.slice(-1);
let ordinalSuffix = 'th';
if (stringFinalDigit === '1' && stringDay !== '11') ordinalSuffix = 'st';
if (stringFinalDigit === '2' && stringDay !== '12') ordinalSuffix = 'nd';
if (stringFinalDigit === '3' && stringDay !== '13') ordinalSuffix = 'rd';
if (daysOfMonth.length > 1 && index === daysOfMonth.length - 1) dayStringArray.push(' and');
dayStringArray.push(`${stringDay}${ordinalSuffix}`);
if (daysOfMonth.length > 2 && index !== daysOfMonth.length - 1) dayStringArray.push(',');
});
dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) {
switch (weeksOfMonth[0]) {
case 0:
dayStringArray.push('first');
break;
case 1:
dayStringArray.push('second');
break;
case 2:
dayStringArray.push('third');
break;
case 3:
dayStringArray.push('fourth');
break;
case 4:
dayStringArray.push('fifth');
break;
default:
break;
}
activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
}
break;
case 'yearly':
return ` on <strong>${moment(startDate).format('MMMM Do')}</strong>`;
default:
return '';
}
return dayStringArray.join('');
},
formattedDifficulty (priority) {
switch (priority) {
case 0.1:
return 'a <strong>trivial</strong>';
case 1:
return 'an <strong>easy</strong>';
case 1.5:
return 'a <strong>medium</strong>';
case 2:
return 'a <strong>hard</strong>';
default:
return null;
}
},
formattedRepeatInterval (frequency, everyX) {
const numericX = Number(everyX);
switch (frequency) {
case 'daily':
if (numericX === 1) return '<strong>every day</strong>';
if (numericX === 2) return '<strong>every other day</strong>';
return `<strong>every ${numericX} days</strong>`;
case 'weekly':
if (numericX === 1) return '<strong>every week</strong>';
if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`;
case 'monthly':
if (numericX === 1) return '<strong>every month</strong>';
if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly':
if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`;
default:
return null;
}
},
},
};
</script>
+48 -33
View File
@@ -1,5 +1,8 @@
<template>
<div class="row user-tasks-page">
<div
class="row user-tasks-page"
@click="openCreateBtn ? openCreateBtn = false : null"
>
<broken-task-modal />
<task-modal
ref="taskModal"
@@ -7,6 +10,11 @@
:purpose="creatingTask !== null ? 'create' : 'edit'"
@cancel="cancelTaskModal()"
/>
<task-summary
ref="taskSummary"
:task="editingTask"
@cancel="cancelTaskModal()"
/>
<div class="col-12">
<div class="row tasks-navigation">
<div class="col-12 col-md-4 offset-md-4">
@@ -160,45 +168,43 @@
</div>
</div>
</div>
<div class="create-task-area d-flex">
<transition name="slide-tasks-btns">
<div class="create-task-area">
<div
id="create-task-btn"
class="btn btn-primary create-btn d-flex align-items-center"
:class="{open: openCreateBtn}"
@click.stop.prevent="openCreateBtn = !openCreateBtn"
@keypress.enter="openCreateBtn = !openCreateBtn"
tabindex="0"
>
<div
v-if="openCreateBtn"
class="d-flex"
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1"> {{ $t('addTask') }} </div>
</div>
<div
v-if="openCreateBtn"
class="dropdown"
>
<div
v-for="type in columns"
:key="type"
@click="createTask(type)"
class="dropdown-item d-flex px-2 py-1"
>
<div
v-for="type in columns"
:key="type"
v-b-tooltip.hover.bottom="$t(type)"
class="create-task-btn diamond-btn"
@click="createTask(type)"
>
<div class="d-flex align-items-center justify-content-center task-icon">
<div
class="svg-icon"
class="svg-icon m-auto"
:class="`icon-${type}`"
v-html="icons[type]"
></div>
</div>
<div class="task-label ml-2">
{{ $t(type) }}
</div>
</div>
</transition>
<div
id="create-task-btn"
class="create-btn diamond-btn btn btn-success"
:class="{open: openCreateBtn}"
@click="openCreateBtn = !openCreateBtn"
>
<div
class="svg-icon"
v-html="icons.positive"
></div>
</div>
<b-tooltip
v-if="!openCreateBtn"
target="create-task-btn"
placement="bottom"
>
{{ $t('addTask') }}
</b-tooltip>
</div>
</div>
<div class="row tasks-columns">
@@ -211,6 +217,7 @@
:search-text="searchTextThrottled"
:selected-tags="selectedTags"
@editTask="editTask"
@taskSummary="taskSummary"
@openBuyDialog="openBuyDialog($event)"
/>
</div>
@@ -345,7 +352,7 @@
}
.create-task-area {
top: -2.5rem;
top: 1px;
}
.drag {
@@ -381,6 +388,7 @@ import cloneDeep from 'lodash/cloneDeep';
import draggable from 'vuedraggable';
import TaskColumn from './column';
import TaskModal from './taskModal';
import TaskSummary from './taskSummary';
import spells from './spells';
import markdown from '@/directives/markdown';
@@ -401,6 +409,7 @@ export default {
components: {
TaskColumn,
TaskModal,
TaskSummary,
spells,
brokenTaskModal,
draggable,
@@ -524,13 +533,19 @@ export default {
};
this.newTag = null;
},
// Need Vue.nextTick() otherwise the first time the modal is not rendered
editTask (task) {
this.editingTask = cloneDeep(task);
// Necessary otherwise the first time the modal is not rendered
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
taskSummary (task) {
this.editingTask = cloneDeep(task);
Vue.nextTick(() => {
this.$root.$emit('bv::show::modal', 'task-summary');
});
},
createTask (type) {
this.openCreateBtn = false;
this.creatingTask = taskDefaults({ type, text: '' }, this.user);
@@ -84,6 +84,7 @@
import moment from 'moment';
import { mapState } from '@/libs/store';
import scoreTask from '@/mixins/scoreTask';
import sync from '@/mixins/sync';
import Task from './task';
import LoadingSpinner from '../ui/loadingSpinner';
@@ -92,7 +93,7 @@ export default {
Task,
LoadingSpinner,
},
mixins: [scoreTask],
mixins: [scoreTask, sync],
props: {
yesterDailies: {
type: Array,
@@ -180,6 +181,7 @@ export default {
this.isLoading = false;
this.$root.$emit('bv::hide::modal', 'yesterdaily');
if (this.$route.fullPath.indexOf('task-information') !== -1) this.sync();
},
},
};
+5
View File
@@ -29,6 +29,11 @@ export default [
type: 'Staff',
uuid: '61b2c855-0a30-444c-bcc6-1cac876460b0',
},
{
name: 'heyeilatan',
type: 'Staff',
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
},
{
name: 'Alys',
type: 'Moderator',
@@ -17,8 +17,14 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
};
},
methods: {
hideSet (set) {
return moment(appearanceSets[set.key].availableUntil).isBefore(moment());
hideSet (setKey) {
if (appearanceSets[setKey].availableFrom) {
return !moment().isBetween(
appearanceSets[setKey].availableFrom,
appearanceSets[setKey].availableUntil,
);
}
return moment(appearanceSets[setKey].availableUntil).isBefore(moment());
},
mapKeysToFreeOption (key, type, subType) {
const userPreference = subType
@@ -43,8 +49,8 @@ export const avatarEditorUtilies = { // eslint-disable-line import/prefer-defaul
const locked = !userPurchased || !userPurchased[key];
let hide = false;
if (set && appearanceSets[set]) {
if (locked) hide = moment(appearanceSets[set].availableUntil).isBefore(moment());
if (set && appearanceSets[set] && locked) {
hide = this.hideSet(set);
}
option.gemLocked = locked;
+2 -18
View File
@@ -15,24 +15,8 @@ export default {
}),
},
methods: {
async beforeTaskScore (task) {
const { user } = this;
if (this.castingSpell) return false;
if (task.group.approval.required && !task.group.approval.approved) {
task.group.approval.requested = true;
const { data: groupPlans } = await this.$store.dispatch('guilds:getGroupPlans');
const groupPlan = groupPlans.find(g => g.id === task.group.id);
if (groupPlan) {
const managers = Object.keys(groupPlan.managers);
managers.push(groupPlan.leader);
if (managers.indexOf(user._id) !== -1) {
task.group.approval.approved = true;
}
}
}
return true;
async beforeTaskScore () {
return (!this.castingSpell);
},
playTaskScoreSound (task, direction) {
switch (task.type) { // eslint-disable-line default-case
+32
View File
@@ -0,0 +1,32 @@
import clone from 'lodash/clone';
export default {
methods: {
async syncTask () {
if (this.groupId || this.task?.group.id) {
const members = await this.$store.dispatch('members:getGroupMembers', {
groupId: this.groupId || this.task?.group.id,
includeAllPublicFields: true,
});
this.members = members;
this.membersNameAndId = [];
this.members.forEach(member => {
this.membersNameAndId.push({
id: member._id,
name: member.profile.name,
addlText: `@${member.auth.local.username}`,
});
this.memberNamesById[member._id] = member.profile.name;
});
this.assignedMembers = [];
if (this.task?.group?.assignedUsers) {
this.assignedMembers = this.task.group.assignedUsers;
}
}
// @TODO: Task modal component is mutating a prop
// and that causes issues. We need to not copy the prop similar to group modals
if (this.task) this.checklist = clone(this.task.checklist);
},
},
};
+1 -1
View File
@@ -204,7 +204,7 @@ export async function createGroupTasks (store, payload) {
}
export async function assignTask (store, payload) {
const response = await axios.post(`/api/v4/tasks/${payload.taskId}/assign/${payload.userId}`);
const response = await axios.post(`/api/v4/tasks/${payload.taskId}/assign`, payload.assignedUserIds);
return response.data.data;
}
+26 -19
View File
@@ -84,7 +84,8 @@ export function canEdit (store) {
const user = store.state.user.data;
const userId = user.id || user._id;
const isUserAdmin = user.permissions && user.permissions.challengeAdmin;
const isUserAdmin = user.permissions
&& (user.permissions.challengeAdmin || user.permissions.fullAccess);
const isUserGroupLeader = group && (group.leader
&& group.leader._id === userId);
const isUserGroupManager = group && (group.managers
@@ -101,11 +102,7 @@ export function canEdit (store) {
}
break;
case 'group':
if (!onUserDashboard) {
isUserCanEditTask = isUserGroupLeader || isUserGroupManager || isUserAdmin;
} else {
isUserCanEditTask = true;
}
isUserCanEditTask = isUserGroupLeader || isUserGroupManager;
break;
default:
break;
@@ -114,15 +111,20 @@ export function canEdit (store) {
};
}
function _nonInteractive (task) {
return (task.group && task.group.id && !task.userId)
|| (task.challenge && task.challenge.id && !task.userId)
|| (task.group && task.group.approval && task.group.approval.requested
&& task.type !== 'habit');
function _nonInteractive (task, userId) {
if (task.userId) return false;
if (task.challenge && task.challenge.id) return true;
if (
task.group && task.group.assignedUsers
&& task.group.assignedUsers.length > 0
&& task.group.assignedUsers.indexOf(userId) === -1
) return true;
return false;
}
export function getTaskClasses (store) {
const userPreferences = store.state.user.data.preferences;
const userId = store.state.user.data._id;
// Purpose can be one of the following strings:
// Edit Modal: edit-modal-bg, edit-modal-text, edit-modal-icon
@@ -169,9 +171,14 @@ export function getTaskClasses (store) {
case 'control':
if (type === 'todo' || type === 'daily') {
if (task.completed || (!shouldDo(dueDate, task, userPreferences) && type === 'daily')) {
if (task.completed
|| (!shouldDo(dueDate, task, userPreferences) && type === 'daily')
|| (task.group && task.group.assignedUsersDetail
&& task.group.assignedUsersDetail[userId]
&& task.group.assignedUsersDetail[userId].completed)
) {
return {
bg: _nonInteractive(task) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
bg: _nonInteractive(task, userId) ? 'task-disabled-daily-todo-control-bg-noninteractive' : 'task-disabled-daily-todo-control-bg',
checkbox: 'task-disabled-daily-todo-control-checkbox',
inner: 'task-disabled-daily-todo-control-inner',
content: 'task-disabled-daily-todo-control-content',
@@ -179,28 +186,28 @@ export function getTaskClasses (store) {
}
return {
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
checkbox: `task-${color}-control-checkbox`,
inner: `task-${color}-control-inner-daily-todo`,
icon: `task-${color}-control-icon`,
};
} if (type === 'reward') {
return {
bg: _nonInteractive(task) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
bg: _nonInteractive(task, userId) ? 'task-reward-control-bg-noninteractive' : 'task-reward-control-bg',
};
} if (type === 'habit') {
return {
up: task.up
? {
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
icon: `task-${color}-control-icon`,
}
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
down: task.down
? {
bg: _nonInteractive(task) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
inner: _nonInteractive(task) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
bg: _nonInteractive(task, userId) ? `task-${color}-control-bg-noninteractive` : `task-${color}-control-bg`,
inner: _nonInteractive(task, userId) ? `task-${color}-control-inner-habit-noninteractive` : `task-${color}-control-inner-habit`,
icon: `task-${color}-control-icon`,
}
: { bg: 'task-disabled-habit-control-bg', inner: 'task-disabled-habit-control-inner', icon: `task-${color}-control-icon` },
@@ -35,7 +35,10 @@ describe('canEdit getter', () => {
});
it('can Edit task in own dashboard', () => {
expect(store.getters['tasks:canEdit'](task, 'challenge', true, null, challenge)).to.equal(true);
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(true);
});
it('cannot Edit group task in own dashboard', () => {
expect(store.getters['tasks:canEdit'](task, 'group', true, group, null)).to.equal(false);
});
it('can Edit any challenge task if admin', () => {
@@ -143,7 +143,14 @@ describe('getTaskClasses getter', () => {
});
it('returns noninteractive classes and padlock icons for group board tasks', () => {
const task = { type: 'todo', value: 2, group: { id: 'group-id' } };
const task = {
type: 'todo',
value: 2,
group: {
id: 'group-id',
assignedUsers: ['not-me'],
},
};
expect(getTaskClasses(task, 'control')).to.deep.equal({
bg: 'task-good-control-bg-noninteractive',
checkbox: 'task-good-control-checkbox',
-1
View File
@@ -23,7 +23,6 @@ const envVars = [
'BASE_URL',
'GA_ID',
'STRIPE_PUB_KEY',
'FACEBOOK_KEY',
'GOOGLE_CLIENT_ID',
'APPLE_AUTH_CLIENT_ID',
'AMPLITUDE_KEY',
+46 -1
View File
@@ -1969,5 +1969,50 @@
"weaponSpecialWinter2021MageText": "Kouzelný měsíční phaser",
"weaponSpecialWinter2021MageNotes": "Tato mocná zbraň je rozhodně víc než jen phaser. Usměrni svou energii, zaměř se na průběh měsíce a studuj časoprostor. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Výzbroj z limitované edice pro zimu 2020-2021.",
"weaponSpecialSpring2021MageNotes": "Hoď, poraž, šlapej, odpočívej! Pírko sviští časem, aby dirigovalo hudbu tvých kouzel. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Výzbroj z limitované edice pro jaro 2021.",
"weaponSpecialSpring2021HealerNotes": "Kůra a listy této čerstvě uříznuté větve jsou známy pro jejich schopnost zmírnění bolesti. Nebo můžeš větev zasadit a dívat se, jak roste! Zvyšuje inteligenci o <%= int %>. Výzbroj z limitované edice pro jaro 2021."
"weaponSpecialSpring2021HealerNotes": "Kůra a listy této čerstvě uříznuté větve jsou známy pro jejich schopnost zmírnění bolesti. Nebo můžeš větev zasadit a dívat se, jak roste! Zvyšuje inteligenci o <%= int %>. Výzbroj z limitované edice pro jaro 2021.",
"weaponSpecialSummer2021RogueNotes": "Jakékoliv dravé monstrum jenž se opováží přiblížit pocítí bodnutí tvých ochranných přátel. Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2021.",
"weaponSpecialSummer2021WarriorText": "Vodní čepel",
"weaponSpecialSummer2022RogueNotes": "Pokud jsi v nouzi, neváhej ukázat tato hrůzostrašná klepeta! Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2022.",
"weaponSpecialSummer2022WarriorNotes": "Točí se to! Mění to směr! A přináší to bouři! Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2022.",
"weaponSpecialSummer2022MageText": "Hůl Manty obrovské",
"weaponSpecialSummer2022MageNotes": "Magicky vyčisti vodu před tebou jedním mávnutím touto hůlkou. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice 2022 letní výzbroj.",
"weaponSpecialSummer2022HealerNotes": "Tyto bubliny uvolňují do vody léčivou magii s uspokojujícím praskáním. Zvyšuje inteligenci o <%= int %>. Limitovaná edice letní výzbroj 2022.",
"weaponSpecialFall2021MageNotes": "Znalosti hledají znalosti. Tato hrůzostrašná ruka, vytvořená ze vzpomínek a tužeb, se snaží získat víc. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice podzimní výzbroj 2021.",
"weaponSpecialSummer2022RogueText": "Krabí klepeto",
"weaponSpecialWinter2022HealerNotes": "Dotkni se tímto vodním náčiním krku přátel a oni vyskočí ze židle! Pak se však budou cítit lépe. Doufejme. Zvyšuje inteligenci o <%= int %>. Limitovaná edice zimní výzbroj 2021-2022.",
"weaponSpecialWinter2022RogueNotes": "Stříbrňáky a zlaťáky zloději milují, že? Tyto zbraně jsou tedy zcela namístě. Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2021-2022.",
"weaponSpecialSummer2022WarriorText": "Vířivá cyklóna",
"weaponSpecialSummer2022HealerText": "Blahodárné bubliny",
"weaponSpecialWinter2022MageNotes": "Bobule na této hůlce obsahují starodávné kouzlo, jenž lze ovládat v zimě. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice zimní výzbroj 2021-2022.",
"weaponSpecialSummer2021WarriorNotes": "Tato třpytivá čepel dokáže plynout jako voda, nicméně dokáže i proříznout do jádra těch nejzáludnějších problémů. Zvyšuje sílu o <%= str %>. Limitovaná edice letní výzbroj 2021.",
"weaponSpecialSummer2021MageText": "Nautiloidní hůl",
"weaponSpecialSummer2021MageNotes": "Nezáleží zda tvé magické ambice sahají do nesmírné hloubky nebo zda se chceš pouze nořit v mělčinách magie. Tento zářivý nástroj ti v obou případech dobře poslouží. Zvyšuje inteligenci o <%= int %> a vnímavost o <%= per %>. Limitovaná edice letní výzbroj 2021.",
"weaponSpecialSummer2021HealerText": "Kukuřičná hůl",
"weaponSpecialWinter2022RogueText": "Ohňostroj padající hvězdy",
"weaponSpecialWinter2022WarriorText": "Meč z cukrové třtiny",
"weaponSpecialWinter2022WarriorNotes": "Kolik líznutí je potřeba k nabroušení této cukrové třtiny do perfektního meče? Zvyšuje sílu o <%= str %>. Limitovaná edice zimní výzbroj 2021-2022.",
"weaponSpecialWinter2022MageText": "Hůl z granátového jablka",
"weaponSpecialWinter2022HealerText": "Hůl z ledového krystalu",
"weaponSpecialFall2021RogueText": "Kapající sliz",
"weaponSpecialFall2021RogueNotes": "Do čeho jsi se to proboha dostal? Když se říká že zloději mají lepkavé prsty, tohle se tím nemyslí! Zvyšuje sílu o <%= str %>. Limitovaná edice podzimní výzbroj 2021.",
"weaponSpecialFall2021WarriorText": "Sekera jezdce na koni",
"weaponSpecialFall2021WarriorNotes": "Tato stylová jednočepelová sekera je ideální na sekání.. dýní! Zvyšuje sílu o <%= str %>. Limitovaná edice podzimní výzbroj 2021.",
"weaponSpecialFall2021MageText": "Hůl čisté myšlenky",
"weaponSpecialFall2021HealerText": "Hůl přivolávání",
"weaponSpecialFall2021HealerNotes": "Využij tuto hůl k přivolání léčivých plamenů a duchů, jenž ti pomohou. Zvyšuje inteligenci o <%= int %>. Limitovaná edice podzimní výbava 2021.",
"weaponSpecialSpring2022WarriorNotes": "Jejda! Hádám že ten vítr byl trošku silnější než jsi si myslel, že? Zvyšuje sílu o <%= str %>. Limitovaná edice jarní výzbroj 2022.",
"weaponSpecialSpring2022MageText": "Zlaticová hůl",
"weaponSpecialSpring2022MageNotes": "Tyto zářivě žluté zvonky jsou připraveny nasměrovat tvoji mocnou jarní magii. Zvyšuje inteligenci o <%= int %> a vnímání o <%= per %>. Limitovaná edice jarní výzbroj 2022.",
"weaponSpecialSpring2022HealerNotes": "Použij tuto hůlku k využití léčivých vlastností periodotu, ať už to má přinést klid, pozitivitu nebo laskavost. Zvyšuje inteligenci o <%= int %>. Limitovaná edice jarní výzbroj 2022.",
"weaponSpecialSpring2022RogueText": "Obrovský narozeninový puzet",
"weaponSpecialSpring2022RogueNotes": "Lesk! Je tak lesklá a třpytivá a hezká a krásná a celá tvoje! Zvyšuje sílu o <%= str %>. Limitovaná edice jarní výzbroj 2022.",
"weaponSpecialSpring2022WarriorText": "Deštník naruby",
"weaponSpecialSpring2022HealerText": "Peridotová hůlka",
"headSpecialNye2021Notes": "Obdržel jsi Absurdní party čepici! Nasaď si ji s pýchou zatímco odbíjí Nový rok. Nepřináší žádné výhody.",
"headSpecialNye2021Text": "Absurdní party čepice",
"weaponSpecialWinter2021HealerNotes": "Vyraž do bitev s fanfárou a závanem větru! Zvyšuje inteligenci o <%= int %>. Limitovaná edice zimní výzbroj 2020-2021.",
"weaponMystery202102Notes": "Zářivý růžový drahokem v této hůlce dokáže šířit radost a přátelství všude možně! Nepřináší žádné výhody. Předmět pro předplatitele, únor 2021.",
"weaponMystery202104Notes": "Tvoji nepřátele by si měli dávat pozor - máš totiž mocnou a pichlavou obranu! Nepřináší žádné výhody. Předmět pro předplatitele duben 2021.",
"weaponMystery202102Text": "Okouzlující hůlka",
"weaponMystery202104Text": "Hůl trnitého bodláku"
}
+15 -2
View File
@@ -118,11 +118,24 @@
"achievementDomesticatedText": "Har udklækket følgende tæmmede kæledyr i alle standardfarver: Fritte, Marsvin, Hane, Flyvende Gris, Rotte, Kanin, Hest, og Ko!",
"achievementDomesticated": "E-I-E-I-O",
"achievementDomesticatedModalText": "Du har indsamlet alle tæmmede kæledyr!",
"achievementZodiacZookeeperText": "Har udklækket alle kæledyr, der er et kinesisk stjernetegn: Rotte, Ko, Kanin, Slange, Hest, Får, Abe, Hane, Ulv, Tiger, Flyvende Gris, og Drage!",
"achievementZodiacZookeeperText": "Har udklækket alle kæledyr, der er et kinesisk stjernetegn, i alle standardfarver: Rotte, Ko, Kanin, Slange, Hest, Får, Abe, Hane, Ulv, Tiger, Flyvende Gris, og Drage!",
"achievementZodiacZookeeperModalText": "Du har samlet alle dyr, der er et stjernetegn!",
"achievementShadyCustomerText": "Har samlet alle Skyggekæledyr.",
"achievementShadeOfItAllText": "Har tæmmet alle Skyggeridedyr.",
"achievementShadeOfItAllModalText": "Du har tæmmet alle Skyggeridedyr!",
"achievementShadyCustomer": "Lyssky kunde",
"achievementShadyCustomerModalText": "Du har indsamlet alle Skygge Dyr!"
"achievementShadyCustomerModalText": "Du har indsamlet alle Skygge Dyr!",
"achievementGroupsBeta2022ModalText": "Du og dine grupper hjalp Habitica ved at teste funktioner og komme med feedback!",
"achievementGroupsBeta2022": "Interaktiv betatester",
"achievementGroupsBeta2022Text": "Du og din gruppe kom med uvurderlig feedback under en test af Habitica.",
"achievementWoodlandWizard": "Skovheks",
"achievementWoodlandWizardText": "Har udklækket alle skovens dyr i alle standardfarver: Grævling, Bjørn, Hjort, Ræv, Frø, Pindsvin, Ugle, Snegl, Egern og Træling!",
"achievementWoodlandWizardModalText": "Du har samlet alle skovkæledyr!",
"achievementReptacularRumbleText": "Has udklækket alle reptilkæledyr i alle standardfarver: Alligator, Flyveøgle, Slange, Triceratops, Skildpadde, Tyrannosaurus Rex og Velociraptor!",
"achievementReptacularRumbleModalText": "Du har samlet alle reptilkæledyr!",
"achievementBirdsOfAFeather": "Én fjer, fem høns",
"achievementBirdsOfAFeatherModalText": "Du har samlet alle de flyvende kæledyr!",
"achievementBirdsOfAFeatherText": "Har udklækket alle flyvende kæledyr i alle standardfarver: Flyvende Gris, Ugle, Papegøje, Flyveøgle, Grif, Falk, Påfugl og Hane!",
"achievementZodiacZookeeper": "Dyrekredsens dyretæmmer",
"achievementShadeOfItAll": "Skyggen af det hele"
}
+2 -1
View File
@@ -492,5 +492,6 @@
"backgroundParkWithStatueText": "Park med Statue",
"backgroundDojoNotes": "Lær nye teknikker i en Dojo.",
"backgroundDojoText": "Dojo",
"backgrounds052019": "SET 60: Frigivet Maj 2019"
"backgrounds052019": "SET 60: Frigivet Maj 2019",
"hideLockedBackgrounds": "Skjul låste baggrunde"
}
+2 -1
View File
@@ -103,5 +103,6 @@
"selectParticipant": "Vælg en deltager",
"filters": "Filtre",
"wonChallengeDesc": "<%= challengeName %> udvalgte dig som vinder! Din sejr er noteret i dine Præstationer.",
"yourReward": "Din Belønning"
"yourReward": "Din Belønning",
"removeTasks": "Fjern opgaver"
}
@@ -2,25 +2,25 @@
"tavernCommunityGuidelinesPlaceholder": "Venlig påmindelse: Det her er en chat for alle aldre, så hold venligst en passende tone og vær opmærksom på indholdet af dine beskeder! Læs Retningslinjer for fællesskabet i sidepanelet, hvis du har spørgsmål.",
"lastUpdated": "Sidst opdateret:",
"commGuideHeadingWelcome": "Velkommen til Habitica!",
"commGuidePara001": "Vær hilset, eventyrer! Velkommen til Habitica, et land af produktivitet, sund levemåde, og til tider en enkelt hærgende grif. Vi er et muntert fællesskab fuld af hjælpsomme folk der støtter op om hinanden på deres vej til at forbedre sig selv. For at passe ind har du kun brug for en positiv attitude, respektfuld opførsel, og forståelse for at alle har forskellige evner of begrænsninger -- inklusiv dig! Habiticanere er tålmodige med hinanden og forsøger at hjælpe hvor de kan.",
"commGuidePara002": "For at sikre at alle er trygge, glade, og produktive i vores fællesskab, har vi nogle retningslinjer. Vi har omhyggeligt konstrueret dem så de er så venlige og letlæselige som muligt. Brug venligt et øjeblik på at læse dem inden du begynder at chatte.",
"commGuidePara003": "Disse regler gælder alle de sociale områder vi bruger, inklusiv (men ikke nødvendigvis begrænset til) Trello, GitHub, Weblate og Wikia (wiki'en). Sommetider vil der opstå uforudsete situationer, eksempelvis en ny kilde til konflikt eller en ondskabsfuld åndemaner. Når det sker, kan moderatorer reagere ved at ændre disse retningslinjer for at beskytte fællesskabet mod ny trusler. Frygt ikke: Du vil blive gjort opmærksom på ændringer i retningslinjerne via en påmindelse fra Bailey.",
"commGuidePara001": "Vær hilset, eventyrer! Velkommen til Habitica, et land af produktivitet, sund levemåde, og til tider en enkelt hærgende grif. Vi er et muntert fællesskab fuld af hjælpsomme folk der støtter op om hinanden på deres vej til at forbedre sig selv. For at passe ind har du kun brug for en positiv attitude, respektfuld opførsel, og forståelse for at alle har forskellige evner of begrænsninger - inklusiv dig! Habiticanere er tålmodige med hinanden og forsøger at hjælpe hvor de kan.",
"commGuidePara002": "For at sikre at alle er trygge, glade, og produktive i vores fællesskab, har vi nogle retningslinjer. Vi har omhyggeligt konstrueret dem så de er så venlige og letlæselige som muligt. Brug venligst et øjeblik på at læse dem inden du begynder at chatte.",
"commGuidePara003": "Disse regler gælder alle de sociale områder vi bruger, inklusiv (men ikke nødvendigvis begrænset til) Trello, GitHub, Weblate og Habitica wiki'en på fandom.com. Efterhånden som Habitica vokser og forandres, kan det ske at vores regler bliver ændret. Når der er sket bemærkelsesværdige ændringer af retningslinjerne for fællesskabet, vil du kunne høre om det i en meddelelse fra Bailey og/eller på vores sociale medier!",
"commGuideHeadingInteractions": "Interaktioner i Habitica",
"commGuidePara015": "Habitica har to slags sociale rum: offentlige og private. Offentlige rum inkluderer Værtshuset, Offentlige klaner, GitHub, Trello og Wiki'en. Private rum er Private klaner, Holdchatten og Private beskeder. Alle display names skal følge retningslinjerne for offentlige rum. Gå til Bruger > Indstillinger > Side for at ændre dit brugernavn.",
"commGuidePara016": "Når du navigerer rundt i de offentlige steder af Habitica, er der nogle generelle regler for at sørge for, at alle er sikre og glade. De burde være lette for eventyrere som dig!",
"commGuideList02A": "<strong>Respekter hinanden</strong>. Vær høflig, venlig og hjælpsom. Husk: Habiticanere kommer fra alle baggrunde og har haft vildt skiftende oplevelser og erfaringer. Det er en del af det, der gør Habitica så cool! At bygge et fællesskab betyder at vi respekterer og fejrer vores forskelle såvel som vores ligheder. Her er nogle nemme måder at vise respekt for hinanden på:",
"commGuideList02B": "<strong>Adlyd alt i <a href='/static/terms' target='_blank'>Vilkår og betingelser</a></strong>.",
"commGuideList02C": "<strong>Læg ikke billeder eller tekst op, der er voldeligt, truende eller seksuelt eksplicit/antydende, eller som promoverer diskrimination, fordomme, racisme, sexisme, had, chikane eller skade mod et individ eller en gruppe</strong>. Ikke engang som en joke. Dette inkluderer skældsord såvel som udtalelser. Ikke alle har den samme form for humor, så noget du anser som en joke er muligvis sårende for en anden. Angrib jeres Daglige opgaver, ikke hinanden.",
"commGuideList02D": "<strong>Hold diskussioner passende for alle aldre</strong>. Vi har mange unge Habiticanere der bruger hjemmesiden! Lad os ikke genere nogen uskyldige eller forhindre nogen Habiticanere i at opnår deres mål.",
"commGuideList02E": "<strong>Undgå bandeord. Dette inkluderer mildere, religiøst baserede bandeord der kan være acceptable andre steder</strong>. Vi har folk fra alle religiøse og kulturelle baggrunde, og vi vil gerne sørge for at de alle føler sig trygge i offentlige rum. <strong>Hvis en moderator eller medarbejder fortæller dig at et udtryk ikke er tilladt i Habitica, selvom det er et udtryk, du ikke vidste var problematisk, er den beslutning endegyldig</strong>. Der vil blive slået særlig hårdt ned på skældsord, da de også går imod vores Vilkår og betingelser.",
"commGuideList02F": "<strong>Undgå lange diskussioner af opsplittende emner i Værtshuset og hvor det ellers ikke ville være et passende emne</strong>. Hvis du føler nogen har sagt noget uhøfligt eller sårende, så svar dem ikke. Hvis nogen nævner noget der er tilladt ifølge retningslinjerne, men sårende for dig, er det okay høfligt at sige det til dem. Hvis det er imod retningslinjerne eller Vilkår og betingelser, så rapporter det og lad en moderator håndtere det. Hvis du er i tvivl, så rapporter beskeden.",
"commGuideList02G": "<strong>Overhold omgående enhver anmodning fra en Moderator.</strong>. Dette kan inkludere, men er ikke begrænset til, at bede dig begrænse antallet af dine beskeder i et bestemt rum, at fjerne upassende indhold fra din profil, at bede dig om at tage din diskussion et mere passende sted osv.",
"commGuideList02J": "<strong>Spam ikke</strong>. Spam inkluderer, men er ikke begrænset til: at skrive den samme kommentar eller spørgsmål flere steder, at lægge links op uden forklaring eller kontekst, at skrive meningsløse beskeder, at skrive adskillige promoverende beskeder om en Klan, Hold eller Udfordring, eller at skrive mange beskeder på én gang. At tigge om ædelsten eller et abonnement i ethvert chatrum eller via Privatbesked betragtes også som spam.<br/><br/>Det er op til moderatorerne at beslutte om noget er spam eller kan føre til spam, selv hvis du ikke mener du har spammet. For eksempel er det i orden at reklamere for en Klan en enkelt eller to gange, men adskillige beskeder samme da ville sikkert blive betragtet som spam, uanset hvor nyttig Klanen er!",
"commGuidePara015": "Habitica har to slags sociale rum: offentlige og private. Offentlige rum inkluderer Værtshuset, offentlige klaner, GitHub, Trello og wiki'en. Private rum tæller private klaner, Holdchatten og private beskeder. Alle displaynavne og @brugernavne skal følge retningslinjerne for offentlige rum. Gå til Menu > Indstillinger > Profil for at ændre dit displaynavn eller @brugernavn i app'en. På Habiticas hjemmeside skal du gå til Bruger > Indstillinger.",
"commGuidePara016": "Når du navigerer rundt i de offentlige rum af Habitica, er der nogle generelle regler for at sørge for, at alle er sikre og glade.",
"commGuideList02A": "<strong>Respekter hinanden</strong>. Vær høflig, venlig og hjælpsom. Husk: Habiticanere kommer fra alle baggrunde og har haft vildt skiftende oplevelser og erfaringer. Det er en del af det, der gør Habitica så cool! At bygge et fællesskab betyder at vi respekterer og fejrer vores forskelle såvel som vores ligheder.",
"commGuideList02B": "<strong>Adlyd alt i <a href='/static/terms' target='_blank'>Vilkår og betingelser</a></strong> i både offentlige og private rum.",
"commGuideList02C": "<strong>Læg ikke billeder eller tekst op, der er voldeligt, truende eller seksuelt eksplicit/antydende, eller som promoverer diskrimination, fordomme, racisme, sexisme, had, chikane eller skade mod et individ eller en gruppe</strong>. Ikke engang som en joke. Dette inkluderer skældsord såvel som udtalelser. Ikke alle har den samme form for humor, så noget du anser som en joke er muligvis sårende for en anden.",
"commGuideList02D": "<strong>Hold diskussioner passende for alle aldre</strong>. Dette betyder at 18+ emner ikke bør diskuteres i offentlige rum. Vi har mange unge Habiticanere der bruger hjemmesiden, og vores brugere kommer fra mange steder i verden. Vi vil gerne have, at Habitica er så rart og inkluderende et sted som muligt.",
"commGuideList02E": "<strong>Undgå bandeord. Dette inkluderer mildere, religiøst baserede bandeord der kan være acceptable andre steder, og forkortede eller forklædte bandeord.</strong> Vi har folk fra alle religiøse og kulturelle baggrunde, og vi vil gerne sørge for at de alle føler sig trygge i offentlige rum. <strong>Hvis en moderator eller medarbejder fortæller dig at et udtryk ikke er tilladt i Habitica, selvom det er et udtryk, du ikke vidste var problematisk, er den beslutning endegyldig.</strong> Der vil blive slået særlig hårdt ned på skældsord, da de også går imod vores Vilkår og betingelser.",
"commGuideList02F": "Undgå lange diskussioner af opsplittende emner i Værtshuset og hvor det ellers ikke ville være et passende emne. Hvis nogen siger noget der er tilladt under vores retningslinjer, men som er sårende for dig, er det okay høfligt at sige det til dem. Hvis nogen siger til dig, at du har gjort dem ubehageligt til mode, så tag et øjeblik til at tænke over dine ord frem for at svare i vrede. Hvis du føler, at en samtale er ved at blive overophedet, følelsesladet eller sårende, <strong>så hold op med at svare. Rapportér i stedet de relevante beskeder for at sende besked til os.</strong> Moderatorerne vil se på det, så hurtigt de kan. Du kan også sende en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> og er velkommen til at sende screenshots, hvis det ville hjælpe os med at forstå sagen.",
"commGuideList02G": "<strong>Overhold omgående enhver anmodning fra en Moderator.</strong> Dette kan inkludere, men er ikke begrænset til, at bede dig begrænse antallet af dine beskeder i et bestemt rum, at fjerne upassende indhold fra din profil, at bede dig om at tage din diskussion et mere passende sted osv. Begynd ikke at diskutere med moderatorerne. Hvis du har kommentarer til Habiticas moderering, så send en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> for at komme i kontakt med vores community manager.",
"commGuideList02J": "<strong>Spam ikke</strong>. Spam inkluderer, men er ikke begrænset til: at skrive den samme kommentar eller spørgsmål flere steder, <strong>at lægge links op uden forklaring eller kontekst</strong>, at skrive meningsløse beskeder, at skrive adskillige promoverende beskeder om en Klan, Hold eller Udfordring, eller at skrive mange beskeder på én gang. Hvis det at andre brugere følger et link vil give nogen form for afkast eller fordel til dig, skal det stå i beskeden, ellers vil det også blive regnet for spam. Det er op til moderatorerne at beslutte om noget er spam.",
"commGuideList02K": "<strong>Undgå at skrive store overskrifter i offentlige chatrum, især Værtshuset</strong>. Ligesom ALL CAPS læses det som om du råber, og forstyrrer den behagelige stemning.",
"commGuideList02L": "<strong>Vi fraråder kraftigt deling af personlig information -- især information der kan bruges til at identificere dig - i offentlige chatrum</strong>. Identificerende personlig information inkluderer, men er ikke begrænset til: din adresse, emailadresse og din API token/password. Det er for din sikkerheds skyld! Medarbejdere eller moderatorer kan fjerne sådanne beskeder efter forgodtbefindende. Hvis du bliver bedt om personlig information i en privat Klan, Hold eller Besked, anbefaler vi kraftigt at du høfligt afviser og gør medarbejderne og moderatorerne opmærksomme ved enten 1) at rapportere beskeden hvis det er i chatten tilhørende et Hold eller privat Klan, eller 2) at udfylde <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a> og vedlægger screenshots.",
"commGuideList02L": "<strong>Vi fraråder kraftigt deling af personlig information -- især information der kan bruges til at identificere dig - i offentlige chatrum</strong>. Identificerende personlig information inkluderer, men er ikke begrænset til: din adresse, emailadresse og din API token/password. Det er for din sikkerheds skyld! Medarbejdere eller moderatorer kan fjerne sådanne beskeder efter forgodtbefindende. Hvis du bliver bedt om personlig information i en privat Klan, Hold eller Besked, anbefaler vi kraftigt at du høfligt afviser og gør medarbejderne og moderatorerne opmærksomme ved enten 1) at rapportere beskeden, eller 2) at sende en email til <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> og vedhæfte screenshots.",
"commGuidePara019": "<strong>I private rum</strong> har brugere mere frihed til at diskutere hvilke emner de har lyst til, men de må stadig ikke gå imod Vilkår og betingelser, inklusiv at skrive nedværdigende skældsord eller noget diskriminerende, voldeligt eller truende indhold. Bemærk, at fordi navne på Udfordringer kan ses i vinderens offentlige profil skal ALLE navne på Udfrodringer adlyde retningslinjerne for offentlige rum, selv hvis de foregår i et privat rum.",
"commGuidePara020": "<strong>Privatbeskeder</strong> har nogle ekstra retningslinjer. Hvis nogen har blokeret dig må du ikke kontakte dem på andre måder for at bede dem om at fjerne blokeringen. Derudover må du ikke sende privatbeskeder til andre for at bede om hjælp (fordi offentlige svar til spørgsmål om hjælp også kan hjælpe resten af fællesskabet). Sidst men ikke mindst må du ikke sende privatbeskeder til nogen for at tigge om ædelsten eller et abonnement, da dette kan anses som spamming.",
"commGuidePara020A": "<strong>Hvis du ser et indlæg eller en privatbesked, du mener går imod retningslinjerne for offenlige rum, beskrevet ovenfor, eller hvis du ser indlæg eller privatbeskeder, der bekymrer dig eller gør dig utryg, kan du gøre Moderatorer og Medarbejdere opmærksomme på den ved at klikke på flag-ikonet for at rapportere det</strong>. En Medarbejder eller Moderator vil tage sig af situationen, så snart de kan. Bemærk venligst, at det at rapportere uskyldige indlæg går imod disse Retningslinjer (se \"Overtrædelser\" nedenunder). Du kan også kontakte Moderatorerne via formularen “<a href='https://contact.habitica.com/' target='_blank'>Kontakt Moderatorerne</a>. Det kan være det bedste at gøre dette hvis der er flere problematiske indlæg af den samme person i forskellige Klaner, eller hvis situationen kræver en forklaring. Du kan kontakte os på dit eget sprog hvis det er lettere for dig. Vi bliver muligvis nødt til at bruge Google Translate, men vi vil gerne have at du føler dig tryg ved at tage kontakt til os, hvis du har et problem.",
"commGuidePara020": "<strong>Privatbeskeder</strong> har nogle ekstra retningslinjer. Hvis nogen har blokeret dig må du ikke kontakte dem på andre måder for at bede dem om at fjerne blokeringen. Derudover må du ikke sende privatbeskeder til andre for at bede om hjælp (fordi offentlige svar til spørgsmål om hjælp også kan hjælpe resten af fællesskabet). Sidst men ikke mindst må du ikke sende privatbeskeder til nogen for at tigge om nogen form for service, der koster rigtige penge.",
"commGuidePara020A": "<strong>Hvis du ser et indlæg eller en privatbesked, du mener går imod retningslinjerne for offentlige rum, beskrevet ovenfor, eller hvis du ser indlæg eller privatbeskeder, der bekymrer dig eller gør dig utryg, kan du gøre Moderatorer og Medarbejdere opmærksomme på den ved at klikke på flag-ikonet for at rapportere det</strong>. En Medarbejder eller Moderator vil tage sig af situationen, så snart de kan. Bemærk venligst, at det at rapportere uskyldige indlæg går imod disse retningslinjer (se \"Overtrædelser\" nedenunder). Du kan også kontakte Moderatorerne ved at emaile <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>. Det kan være det bedste at gøre dette, hvis der er flere problematiske indlæg af den samme person i forskellige Klaner, eller hvis situationen kræver en forklaring. Du kan kontakte os på dit eget sprog hvis det er lettere for dig. Vi bliver muligvis nødt til at bruge Google Translate, men vi vil gerne have at du føler dig tryg ved at tage kontakt til os, hvis du har et problem.",
"commGuidePara021": "Herudover har nogen offentlige steder i Habitica ekstra retningslinjer.",
"commGuideHeadingTavern": "Værtshuset",
"commGuidePara022": "Værtshuset er det primære sted Habiticanere hænger ud. Kroværten Daniel holder stedet funklende rent, og Lemoness vil med glæde fremtrylle dig et glas lemonade mens du sidder og snakker. Bare husk…",
@@ -31,8 +31,8 @@
"commGuidePara029": "<strong>Offentlige Klaner er meget ligesom Værtshuset, bortset fra at de i stedet for at være til generelle samtaler har et bestemt tema</strong>. Chat i offentlige Klaner bør have fokus på dette tema. For eksempel ville medlemmer af Klanen Wordsmiths måske blive fornærmede, hvis samtalen pludselig handler om havearbejde, og Klanen for Dragefans har nok ikke meget interesse i at tyde oldtidens runer. Nogle Klaner er meget afslappede om dette end andre, men forsøg generelt <strong>at holde dig til emnet</strong>!",
"commGuidePara031": "I nogle offentlige Klaner omtales der følsomme emner som depression, religion, politik osv. Dette er helt i orden, så længe samtalerne i Klanen ikke bryder nogle Vilkår og betingelser eller Regler for offentlige rum, og så længe de holder sig til Klanens emne.",
"commGuidePara033": "<strong>Offentlige Klaner må IKKE indeholde indhold for aldersgruppen 18+. Hvis de planlægger jævnligt at diskutere følsomt indhold, skal dette stå i Klanens beskrivelse</strong>. Dette er for at holde Habitica sikkert of trygt for alle.",
"commGuidePara035": "<strong>Hvis en Klan omhandler flere forskellige følsomme emner, er det hensynsfuldt over for dine med-Habiticanere at skrive din kommentar efter en advarsel (fx \"Advarsel: Omtaler selvskade\")</strong>. Disse kan karakteriseres som trigger warnings og/eller noter om indholdet, og Klaner kan have deres egne regler om disse udover dem, der er givet her. Hvis det er muligt, så brug venligst <a href='http://habitica.fandom.com/wiki/Markdown_Cheat_Sheet' target='_blank'>markdown</a> for at gemme det potentielt følsomme indhold under flere linjeskift, så de, der ikke ønsker at læse det, kan scrolle forbi uden at se indholdet. Habiticas medarbejdere og moderatorer kan stadig fjerne dette indhold efter eget skøn.",
"commGuidePara036": "Desuden bør følsomt indhold stadig være aktuelt for Klanens emne - at tale om selvskade i en Klan hvor fokus er på at kæmpe mod depression giver mening, men er mindre passende i en Klan for musik. Hvis du ser nogen, som gentagne gange går imod denne retningslinje, især efter at være blevet bedt om at stoppe flere gange, så rapportér venligst deres indlæg og gør moderatorerne opmærksom på dette via <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a>.",
"commGuidePara035": "<strong>Hvis en Klan omhandler flere forskellige følsomme emner, er det hensynsfuldt over for dine med-Habiticanere at skrive din kommentar efter en advarsel (fx \"Advarsel: Omtaler selvskade\")</strong>. Disse kan karakteriseres som trigger warnings og/eller noter om indholdet, og Klaner kan have deres egne regler om disse udover dem, der er givet her. Hvis det er muligt, så brug venligst <a href='https://habitica.fandom.com/wiki/Markdown_Cheat_Sheet' target='_blank'>markdown</a> for at gemme det potentielt følsomme indhold under flere linjeskift, så de, der ikke ønsker at læse det, kan scrolle forbi uden at se indholdet. Habiticas medarbejdere og moderatorer kan stadig fjerne dette indhold efter eget skøn.",
"commGuidePara036": "Desuden bør følsomt indhold stadig være aktuelt for Klanens emne - at tale om selvskade i en Klan hvor fokus er på at kæmpe mod depression giver mening, men er mindre passende i en Klan for musik. Hvis du ser nogen, som gentagne gange går imod denne retningslinje, især efter at være blevet bedt om at stoppe flere gange, så rapportér deres beskeder.",
"commGuidePara037": "<strong>Ingen Klaner, hverken Offentlige eller Private, bør oprettes med det formål at angribe en gruppe eller et individ</strong>. At oprette en sådan Klan er grundlæg for øjeblikkelig bortvisning fra Habitica. Nedkæmp dårlige vaner, ikke dine med-eventyrere!",
"commGuidePara038": "<strong>Alle Udfordringer i Værtshuset og Offentlige Klaner skal også følge disse regler</strong>.",
"commGuideHeadingInfractionsEtc": "Overtrædelser, Konsekvenser og Genskabelse",
@@ -44,24 +44,24 @@
"commGuidePara053": "De følgende er eksempler på større overtrædelser. Listen er ikke fyldestgørende.",
"commGuideList05A": "Brud på Betingelser og Vilkår",
"commGuideList05B": "Hadtale/billeder, Chikane/stalking, Cyber-mobning, Flaming og Trolling",
"commGuideList05C": "Brud på Prøvetid",
"commGuideList05D": "At udgive sig for at være Ansat eller Moderator",
"commGuideList05E": "Gentagne Moderate Overtrædelser",
"commGuideList05C": "Brud på prøvetid",
"commGuideList05D": "At udgive sig for at være Ansat eller Moderator - dette inkluderer at påstå, at brugerskabte platforme, der ikke at associerede med Habitica, er officielle og/eller modereret af Habitica eller deres mods/medarbejdere",
"commGuideList05E": "Gentagne moderate overtrædelser",
"commGuideList05F": "Oprettelse af en ekstra konto for at undgå konsekvenser (for eksempel at oprette en ny konto til at chatte med, efter at have fået frataget ens chat-privilegier)",
"commGuideList05G": "Bevidst bedrag af Ansatte eller Moderatorer med det formål at undgå konsekvenser eller bringe en anden bruger i vanskeligheder",
"commGuideHeadingModerateInfractions": "Moderate Overtrædelser",
"commGuideHeadingModerateInfractions": "Moderate overtrædelser",
"commGuidePara054": "Moderate overtrædelser gør ikke fællesskabet usikkert, men de gør det ubehageligt. Disse overtrædelser har moderate konsekvenser. Når de står sammen med andre overtrædelser, kan konsekvenserne blive større.",
"commGuidePara055": "De følgende er eksempler på Moderate Overtrædelser. Listen er ikke endelig.",
"commGuidePara055": "De følgende er eksempler på moderate overtrædelser. Listen er ikke fyldestgørende.",
"commGuideList06A": "At ignorere, opføre respektløst overfor, eller skændes med en Moderator. Dette inkluderer offentlige at klage over moderatorer eller andre brugere, offentligt at forherlige eller forsvare bortviste brugere, eller at debatterer hvorvidt en moderators handlinger var passende. Hvis du er bekymret om en af reglerne eller en Moderators opførsel, så kontakt venligst de ansatte via email (<a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>).",
"commGuideList06B": "Backseat Modding. For at tydeliggøre en relevant pointe: En venlig påmindelse om reglerne er helt fint. Backseat modding består i at fortælle, kræve, og/eller stærkt antyde at nogen bør gøre som du siger for at rette op på en fejl. Du kan gøre nogen opmærksom på at de har begået en overtrædelse, men forlang venligst ikke at de skal gøre noget - for eksempel er det bedre at sige \"Bare så du ved det er det ikke så godt at bande i Værtshuset, så det kan være du skulle slette det\" end \"Jeg er nødt til at bede dig slette det indlæg.\"",
"commGuideList06C": "At rapportere uskyldige indlæg med vilje.",
"commGuideList06D": "Gentagne gange at bryde Retningslinjerne for offentlige rum",
"commGuideList06E": "Gentagne gange at begå mindre overtrædelser",
"commGuideHeadingMinorInfractions": "Mindre Overtrædelser",
"commGuideHeadingMinorInfractions": "Mindre overtrædelser",
"commGuidePara056": "Mindre overtrædelser har kun mindre konsekvenser, men er stadig ikke anbefalede. Hvis de sker gentagne gange kan det føre til større konsekvenser.",
"commGuidePara057": "De følgende er eksempler på Mindre Overtrædelser. Listen er ikke fyldestgørende.",
"commGuidePara057": "De følgende er eksempler på mindre overtrædelser. Listen er ikke fyldestgørende.",
"commGuideList07A": "Førstegangsbrud på Retningslinjer for Offentlige Steder",
"commGuideList07B": "Alle udsagn eller handlinger, som udløser et \"Lad venligst være\". Når en Moderator er nødt til at sige \"Lad venligst være med det\" til en bruger, kan det tælle som en meget lille overtrædelse for den bruger. Et eksempel kunne være \"Lad venligst være med at blive ved med at argumentere for indførslen af den funktion, når vi har sgat til dig adskillige gange at det ikke kan lade sig gøre.\" I mange tilfælde vil 'Lad venligst være' blot være konsekvensen af overtrædelsen, men hvis Moderatorerne er nødt til at bede den samme bruger flere gange om at holde op, vil de Mindre Overtrædelser begynde at tælle som Moderate Overtrædelser.",
"commGuideList07B": "Alle udsagn eller handlinger, som udløser et \"lad venligst være\" fra en moderator. Når du offentligt bliver bedt om at stoppe en handling, kan dette i sig selv tælle som en mindre overtrædelse. Hvis mods er nødt til gentagne gange at irettesætte den samme person, kan det komme til at tælle som en grovere overtrædelse",
"commGuidePara057A": "Nogle indlæg kan blive skjult fordi de indeholder følsom information eller kan give folk det forkerte indtryk. Typisk gælder dette ikke som en overtrædelser, især ikke den første gang det sker!",
"commGuideHeadingConsequences": "Konsekvenser",
"commGuidePara058": "I Habitica - som i virkeligheden - har alle handlinger også konsekvenser, hvad enten det er at komme i bedre form fordi du har løbet, få huller i tænderne fordi du har spist for meget sukker, eller bestå et fag fordi du har studeret.",
@@ -71,53 +71,63 @@
"commGuideList08B": "hvad konsekvensen er",
"commGuideList08C": "hvad der skal til for at rette fejlen og genoprette din status, hvis muligt.",
"commGuidePara060A": "Hvis situationen påkræver det kan du også modtage en PM eller email plus et indlæg i det forum, hvor overtrædelsen blev begået. I nogle tilfælde bliver du slet ikke irettesat offentligt.",
"commGuidePara060B": "Hvis din konto bortvises (en alvorlig konsekvens), vil du ikke være i stand til at logge på Habitica, og vil få en fejl-besked når du prøver. <strong>Hvis du ønsker at undskylde eller argumentere for genoprettelse af din konto, så skriv venligst en email til de ansatte på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> med dit UUID (unikke bruger-ID)</strong> (som vil blive oplyst i fejl-beskeden). Det er <strong>dit</strong> ansvar at tage kontakt, hvis du ønsker en genovervejelse af situationen, eller genoprettelse.",
"commGuideHeadingSevereConsequences": "Eksempler på Større Konsekvenser",
"commGuidePara060B": "Hvis din konto bortvises (en alvorlig konsekvens), vil du ikke være i stand til at logge på Habitica, og vil få en fejl-besked når du prøver. <strong>Hvis du ønsker at undskylde eller argumentere for genoprettelse af din konto, så skriv venligst en email til de ansatte på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> med dit bruger-ID</strong> (som vil blive oplyst i fejlbeskeden) eller @brugernavn. Det er <strong>dit</strong> ansvar at tage kontakt, hvis du ønsker en genovervejelse af situationen, eller genoprettelse.",
"commGuideHeadingSevereConsequences": "Eksempler på større konsekvenser",
"commGuideList09A": "Kontoudelukkelser (se ovenstående)",
"commGuideList09C": "Permanent deaktivering (\"indefrysning\") af fremskridt i Bidragsyder-niveauer",
"commGuideHeadingModerateConsequences": "Eksempler på Moderate Konsekvenser",
"commGuideHeadingModerateConsequences": "Eksempler på moderate konsekvenser",
"commGuideList10A": "Begrænsede offentlige og/eller private chatprivilegier",
"commGuideList10A1": "Hvis dine handlinger resulterer i fradragelse af dine rettigheder til chatten, vil en Moderator eller Ansat sende dig en privatbesked og/eller et indlæg i det forum du blev suspenderet fra for at fortælle dig grunden til og længden af din suspendering. Efter denne periode vil du få dine chatrettigheder tilbage, forudsat at du er villig til at ændre ved den opførsel du blev suspenderet for og overholde Fællesskabets Retningslinjer.",
"commGuideList10A1": "Hvis dine handlinger resulterer i fradragelse af dine rettigheder til chatten, vil en Moderator eller Ansat sende dig en privatbesked og/eller et indlæg i det forum du blev suspenderet fra for at fortælle dig grunden til og længden af din suspendering, og/eller hvad du skal gøre for at få dine chatprivilegier tilbage. Du vil få dem igen, hvis du høfligt følger de anvisninger du får, og erklærer dig villig til at følge vores Retningslinjer for Fællesskabet og Betingelser & Vilkår",
"commGuideList10C": "Begrænsede Klan/Udfordringsoprettelsesprivilegier",
"commGuideList10D": "Midlertidig deaktivering (\"indefrysning\") af fremskridt i Bidragsyder-niveauer",
"commGuideList10E": "Degradering af Bidragsyder-niveauer",
"commGuideList10F": "Sætte brugere på \"Prøvetid\"",
"commGuideHeadingMinorConsequences": "Eksempler på Mindre Konsekvenser",
"commGuideList10F": "At sætte brugere på \"prøvetid\"",
"commGuideHeadingMinorConsequences": "Eksempler på mindre konsekvenser",
"commGuideList11A": "Påmindelser om Retningslinjer for Offentlige Steder",
"commGuideList11B": "Advarsler",
"commGuideList11C": "Anmodninger",
"commGuideList11D": "Slettelser (Moderatorer/Ansatte kan slette problematisk indhold)",
"commGuideList11E": "Rettelser (Moderatorer/Ansatte kan slette problematisk indhold)",
"commGuideList11E": "Rettelser (Moderatorer/Ansatte kan redigere problematisk indhold)",
"commGuideHeadingRestoration": "Genoprettelse",
"commGuidePara061": "Habitica er en land dedikeret til forbedring, og vi tror på at give en chance til <strong>Hvis du begår en overtrædelse og bliver udsat for en konsekvens som følge, så se det som en mulighed for at evaluere dine handlinger og forbedre din opførsel som medlem af fællesskabet</strong>.",
"commGuidePara062": "Den bekendtgørelse, besked og/eller email du modtager, der forklarer konsekvenserne af dine handlinger, er en god kilde til information. Hold dig til de begrænsninger der er blevet påført, og bestræb dig på at møde de krav, der kan få eventuelle straffe ophævet.",
"commGuidePara063": "Hvis du ikke forstår konsekvenserne eller arten af din overtrædelse, så bed Medarbejderne/Moderatorerne om hjælp, så du kan undgå at begå overtrædelser i fremtiden. Hvis du føler at en bestemt beslutning var uretfærdig, kan du kontakte de ansatte for at diskutere det på <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>.",
"commGuideHeadingMeet": "Mød de Ansatte og Moderatorerne!",
"commGuidePara006": "Habitica har nogle utrættelige omvandrende riddere, der kæmper sammen med de ansatta for at holde fællesskabet roligt, tilfreds, og frit for trolde. Hver har et specifikt domæne, men vil sommetider blive kaldt ind for at hjælpe i andre sociale sfærer.",
"commGuidePara006": "Habitica har nogle utrættelige omvandrende riddere, der kæmper sammen med de ansatte for at holde fællesskabet roligt, tilfreds, og frit for trolde. Hver har et specifikt domæne, men vil sommetider blive kaldt ind for at hjælpe i andre sociale sfærer.",
"commGuidePara007": "Ansatte har lilla tags markeret med kroner. Deres titel er \"Heltemodig\".",
"commGuidePara008": "Moderatorer har mørkeblå tags markeret med stjerner. Deres titel er \"Beskytter\". Den eneste undtagelse er Bailey, der som NPC har et sort og grønt tag markeret med en stjerne.",
"commGuidePara009": "De nuværende Ansatte er (fra venstre mod højre):",
"commGuideAKA": "<%= habitName %> aka <%= realName %>",
"commGuidePara008": "Moderatorer har mørkeblå tags markeret med stjerner. Deres titel er \"Beskytter\".",
"commGuidePara009": "De nuværende ansatte er (fra venstre mod højre):",
"commGuideAKA": "<%= habitName %>, aka <%= realName %>",
"commGuideOnTrello": "<%= trelloName %> på Trello",
"commGuideOnGitHub": "<%= gitHubName %> på GitHub",
"commGuidePara010": "Der er også flere Moderatorer, der hjælper de ansatte. Disse er omhyggeligt udvalgt, så vis dem respekt og lyt til deres forslag.",
"commGuidePara011": "De nuværende Moderatorer er (fra venstre mod højre):",
"commGuidePara011b": "På GitHub/Wikia",
"commGuidePara011c": "på Wikia",
"commGuidePara011": "De nuværende moderatorer er (fra venstre mod højre):",
"commGuidePara011b": "på GitHub/Fandom",
"commGuidePara011c": "på wiki'en",
"commGuidePara011d": "på GitHub",
"commGuidePara012": "Hvis du har et problem eller bekymring, der drejer sig om en bestemt Moderator, så send venligst en email til vores Medarbejdere (<a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>).",
"commGuidePara013": "Brugere kommer og går i et fællesskab så stort som Habitica. Sommetider bliver en medarbejder eller moderator nødt til at fralægge sig deres ædle kappe, og slappe lidt af. De følgende er Ansatte og Moderatorer Emeritus. De løfter ikke længere et ansvar som Ansat eller Moderator, men vi vil stadig gerne mindes deres indsats!",
"commGuidePara014": "Ansatte og Moderatorer Emeritus:",
"commGuideHeadingFinal": "Den Sidste Sektion",
"commGuidePara067": "Det var så det, modige Habiticaner - Retningslinjerne for fællesskabet! Tør sveden af panden og giv dig selv nogle Erfaringpoint for at læse det hele. Hvis du har nogle spørgsmål om disse Retningslinjer for fællesskabet, så tag venligst fat i os via <a href='https://contact.habitica.com/' target='_blank'>Moderatorkontaktformularen</a>, og vi vil med glæde forsøge at gøre tingene klart for dig.",
"commGuidePara068": "Tag afsted, modige eventyrer, og bekæmp nogle Daglige!",
"commGuidePara014": "Ansatte og Moderatorer emeritus:",
"commGuideHeadingFinal": "Den sidste sektion",
"commGuidePara067": "Det var så det, modige Habiticaner - Retningslinjerne for fællesskabet! Tør sveden af panden og giv dig selv nogle Erfaringpoint for at læse det hele. Hvis du har nogle spørgsmål om disse Retningslinjer for fællesskabet, så tag venligst fat i os via <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a>, og vi vil med glæde forsøge at gøre tingene klart for dig.",
"commGuidePara068": "Tag afsted, modige eventyrer, og nedkæmp nogle Daglige!",
"commGuideHeadingLinks": "Nyttige links",
"commGuideLink01": "<a href='/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a' target='_blank'>Habitica Help: Ask a Question</a>: en Klan hvor brugere kan stille spørgsmål!",
"commGuideLink02": "<a href='http://habitica.fandom.com/wiki/Habitica_Wiki' target='_blank'>Wiki'en</a>: den største samling af information om Habitica.",
"commGuideLink03": "<a href='https://github.com/HabitRPG/habitica' target='_blank'>GitHub</a>: til bug-rapporter eller hjælp med kodning!",
"commGuideLink02": "<a href='https://habitica.fandom.com/wiki/Habitica_Wiki' target='_blank'>Wiki'en</a>: den største samling af information om Habitica.",
"commGuideLink03": "<a href='https://github.com/HabitRPG/habitica' target='_blank'>GitHub</a>: for at hjælpe med kodning!",
"commGuideLink04": "<a href='https://docs.google.com/forms/d/e/1FAIpQLScPhrwq_7P1C6PTrI3lbvTsvqGyTNnGzp1ugi1Ml0PFee_p5g/viewform?usp=sf_link' target='_blank'>Feedbackformularen</a>: til forslag til hjemmesiden og app'en.",
"commGuideLink05": "<a href='https://trello.com/b/mXK3Eavg/' target='_blank'>Den mobile Trello</a>: til at bede om funktioner til vores apps.",
"commGuideLink06": "<a href='https://trello.com/b/vwuE9fbO/' target='_blank'>Kunst-Trello</a>: til at indsende pixel art.",
"commGuideLink07": "<a href='https://trello.com/b/nnv4QIRX/' target='_blank'>Quest-Trello</a>: til at indsende tekst til quests.",
"commGuidePara069": "Følgende talentfulde kunstnere har bidraget med disse illustrationer:"
"commGuidePara069": "Følgende talentfulde kunstnere har bidraget med disse illustrationer:",
"commGuidePara017": "Her er den hurtige version, men vi opfordrer dig til at læse den mere detaljerede udgave nedenunder:",
"commGuideList01A": "Vores Betingelser & Vilkår gælder alle steder i Habitica, inklusiv private klaner, holdchatten og private beskeder.",
"commGuideList02M": "Bed eller tig ikke andre om at give dig ædelsten, et abonnement eller medlemskab i gruppeplaner. Dette er ikke tilladt at gøre i hverken Værtshuset, offentlige eller private rum, eller i privatbeskeder. Hvis du får en besked med en anmodning om ting, der koster rigtige penge, så rapportér dem venligst. Gentaget eller groft tiggeri, især efter en advarsel er blevet udstedt, kan resulterer i en blokering af din konto.",
"commGuideList01B": "Forbudt: Al kommunikation der er voldeligt, truende, opfordrer til diskrimination osv., inklusiv memes, billeder og jokes.",
"commGuideList01C": "Al samtale skal være passende for enhver aldersgruppe og ikke indeholde skælds- ellers bandeord.",
"commGuideList01D": "Følg venligst moderatorernes anvisninger.",
"commGuideList01E": "Start og deltag ikke i diskussioner i Værtshuset, der kan føre til skænderier.",
"commGuideList01F": "Bed ikke andre om at give dig ting der koster rigtige penge, spam ikke, og skriv ikke beskeder i all caps eller stor overskriftstekst.",
"commGuideList05H": "Grove eller gentagne forsøg på at bedrage eller presse andre spillere til at give dig genstande/services, der koster rigtige penge",
"commGuideList09D": "Fjernelse af Bidragsyder-niveauer"
}
+11 -4
View File
@@ -38,8 +38,8 @@
"questEggHedgehogText": "Pindsvin",
"questEggHedgehogMountText": "Pindsvin",
"questEggHedgehogAdjective": "et stikkende",
"questEggDeerText": "Rådyr",
"questEggDeerMountText": "Rådyr",
"questEggDeerText": "Hjort",
"questEggDeerMountText": "Hjort",
"questEggDeerAdjective": "et elegant",
"questEggEggText": "Æg",
"questEggEggMountText": "Æggekurv",
@@ -200,7 +200,7 @@
"hatchingPotionEmber": "Glødende",
"hatchingPotionThunderstorm": "Tordenvejr",
"hatchingPotionGhost": "Spøgelse",
"hatchingPotionRoyalPurple": "Royal lilla",
"hatchingPotionRoyalPurple": "Purpur",
"hatchingPotionHolly": "Kristtorn",
"hatchingPotionCupid": "Amor",
"hatchingPotionShimmer": "Glimmer",
@@ -365,5 +365,12 @@
"questEggDolphinAdjective": "en munter",
"questEggDolphinMountText": "Delfin",
"questEggDolphinText": "Delfin",
"hatchingPotionDessert": "Konfekt"
"hatchingPotionDessert": "Konfekt",
"hatchingPotionFluorite": "Fluorid",
"hatchingPotionSunset": "Solnedgangs",
"hatchingPotionMoonglow": "Månelys",
"hatchingPotionSolarSystem": "Solsystems",
"hatchingPotionOnyx": "Onyks",
"hatchingPotionPorcelain": "Porcelæns",
"hatchingPotionVirtualPet": "Virtuelt kæledyrs"
}
+3 -2
View File
@@ -49,9 +49,10 @@
"balance": "Balance",
"playerTiers": "Spillertrin",
"tier": "Trin",
"conRewardsURL": "http://habitica.fandom.com/wiki/Contributor_Rewards",
"conRewardsURL": "https://habitica.fandom.com/wiki/Contributor_Rewards",
"surveysSingle": "Hjalp Habitica med at vokse ved at udfylde et spørgeskema eller var en stor hjælp ved test. Tusind tak!",
"surveysMultiple": "Hjalp Habitica med at vokse ved <%= count %> lejligheder, enten ved at udfylde et spørgeskema eller hjalp med et større test-arbejde. Tusind tak!",
"blurbHallPatrons": "Dette er Protektorernes Sal, hvor vi ærer de ædle eventyrere, der støttede Habiticas originale Kickstarter. Vi takker dem for at hjælpe os med at vække Habitica til live!",
"blurbHallContributors": "Dette er Bidragsydernes Sal, hvor dem, der har bidraget med open-source materiale til Habitica, bliver æret. Om det er gennem kode, grafik, musik, tekst eller bare generel hjælpsomhed, har de modtaget <a href='http://habitica.fandom.com/wiki/Contributor_Rewards' target='_blank'> ædelsten, eksklusivt udstyr</a>, og <a href='http://habitica.fandom.com/wiki/Contributor_Titles' target='_blank'>prestigefyldte titler</a>. Du kan også bidrage til Habitica! <a href='http://habitica.fandom.com/wiki/Contributing_to_Habitica' target='_blank'> Find ud af mere her. </a>"
"blurbHallContributors": "Dette er Bidragsydernes Sal, hvor dem, der har bidraget med open-source materiale til Habitica, bliver æret. Om det er gennem kode, grafik, musik, tekst eller bare generel hjælpsomhed, har de modtaget <a href='https://habitica.fandom.com/wiki/Contributor_Rewards' target='_blank'> ædelsten, eksklusivt udstyr</a>, og <a href='https://habitica.fandom.com/wiki/Contributor_Titles' target='_blank'>prestigefyldte titler</a>. Du kan også bidrage til Habitica! <a href='https://habitica.fandom.com/wiki/Contributing_to_Habitica' target='_blank'> Find ud af mere her. </a>",
"noPrivAccess": "Du har ikke de påkrævede rettigheder."
}
+2 -2
View File
@@ -3,7 +3,7 @@
"dontDespair": "Bare rolig!",
"deathPenaltyDetails": "Du mistede et Niveau, dit Guld og et stykke Udstyr, men du kan få det hele igen med hårdt arbejde! Held og lykke - du skal nok klare den.",
"refillHealthTryAgain": "Genfyld Liv og Prøv Igen",
"dyingOftenTips": "Sker dette ofte? <a href='http://habitica.fandom.com/wiki/Death_Mechanics#Strategies_for_Staying_Alive' target='_blank'>Her er nogle fif!</a>",
"dyingOftenTips": "Sker dette ofte? <a href='https://habitica.fandom.com/wiki/Death_Mechanics#Strategies_for_Staying_Alive' target='_blank'>Her er nogle fif!</a>",
"losingHealthWarning": "Pas på - du mister Liv!",
"losingHealthWarning2": "Lad ikke dit Liv falde til nul! Hvis du gør det vil du tabe et niveau, alt dit guld og et stykke udstyr.",
"toRegainHealth": "For at genopfylde Liv:",
@@ -14,4 +14,4 @@
"lowHealthTips4": "Hvis en Daglig ikke er forfalden på en given dag, kan du deaktivere den ved at klikke på blyanten.",
"goodLuck": "Held og lykke!",
"cannotRevive": "Du kan ikke genoplive hvis du ikke er død"
}
}
+2 -2
View File
@@ -3,9 +3,9 @@
"defaultHabit2Text": "Spis junk food (Klik på blyanten for at redigere)",
"defaultHabit3Text": "Tag trapperne/elevatoren (Klik på blyanten for at redigere)",
"defaultHabit4Text": "Tilføj en opgave til Habitica",
"defaultHabit4Notes": "Enten en Vane, en Daglig eller en To-Do",
"defaultHabit4Notes": "Enten en Vane, en Daglig eller en To Do",
"defaultTodo1Text": "Begynd at bruge Habitica (Markér mig som færdig!)",
"defaultTodoNotes": "Du kan enten færdiggøre denne To-Do, ændre den, eller fjerne den.",
"defaultTodoNotes": "Du kan enten færdiggøre denne To Do, ændre den eller fjerne den.",
"defaultReward1Text": "15 minutters pause",
"defaultReward2Text": "Beløn dig selv",
"defaultReward2Notes": "Se fjernsyn, spil et spil, spis noget guf, det er op til dig!",
File diff suppressed because one or more lines are too long

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