Compare commits

...

157 Commits

Author SHA1 Message Date
Phillip Thelen
cd94aeeb1a Improve user history logging 2024-09-06 13:47:41 +02:00
Phillip Thelen
86ea34f0be Add script to notify on heroku deploys (#15286)
* add script for heroku to notify about a deploy

* add emoji to server name

* add fallback for when script is run outside of git repo

* make script use bash

* remove exit
2024-09-06 12:00:00 +02:00
Phillip Thelen
0d0a24deac Formatting fix 2024-09-06 11:59:08 +02:00
Phillip Thelen
3cfe099864 Improve hero saving 2024-09-06 11:58:16 +02:00
Phillip Thelen
cdcaca3188 fix armoire buying issue 2024-09-03 16:28:30 +02:00
Sabe Jones
55e39a1ec9 fix broken test (#15304)
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2024-09-03 16:16:17 +02:00
Natalie
992e82cbcf 2024-09 Content Prebuild (#15295)
* 202409 subscriber gear

* 2024-09 enchanted armoire gear

* 2024 fall festival gear

* 2024-09 background

* 2024-09 pet quest

* typos and update featuredItems.js

* quest and typo fix

* fix subscriber set name

* text amendments

* update quest title
# Conflicts:
#	habitica-images
2024-09-03 15:33:54 +02:00
Phillip Thelen
9b33453040 fix lint 2024-09-03 15:30:50 +02:00
Phillip Thelen
cb865b171b fix tests 2024-09-03 15:15:20 +02:00
Phillip Thelen
68560894b9 improve setting client in history 2024-09-03 15:03:33 +02:00
Phillip Thelen
665b934e35 Improve admin panel stats input 2024-09-03 13:11:53 +02:00
Phillip Thelen
ff53a387d4 allow stats to be edited from admin panel 2024-08-28 16:03:29 +02:00
Phillip Thelen
0bd0ba096b show user history in admin panel 2024-08-28 13:42:19 +02:00
Phillip Thelen
38cad7102f log armoire, quoest response and cron events to history 2024-08-28 12:49:00 +02:00
Natalie
62f5b9698a Add rage button to debug menu (#15291)
* + Rage

* tinkering

* remove if statement wrapper and modify error message

* add test cases

* more work on test cases

* adding contexts to test cases

* test(debug): fix up tests

* fix(lint): whisepate

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:49:00 +02:00
Sabe Jones
8a3824ec02 fix(CI): force apt update before fetching libkrb5-dev 2024-08-28 12:49:00 +02:00
Phillip Thelen
41fa200271 Improve the performance of some frequently used API calls (#15251)
* use lean for getting task lists

* Only load necessary user data for group-plans call

Also don’t make a db request for groups if the user is in none

* Only load necessary user fields for in app rewards

* Optimize updateStore by not checking every item

* Only load necessary user data for task scoring

* improve performance of inbox request calls

* merge fix

* fix scoring task call

* add quests to scoring call

* fix showing official pinned items

* also load achievements
2024-08-28 12:49:00 +02:00
Sabe Jones
91c810925a 5.27.3 2024-08-28 12:49:00 +02:00
Weblate
2887eab3ef Translated using Weblate (French)
Currently translated at 99.7% (792 of 794 strings)

Translated using Weblate (French)

Currently translated at 100.0% (387 of 387 strings)

Translated using Weblate (German)

Currently translated at 91.7% (2892 of 3153 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Turkish)

Currently translated at 92.2% (154 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (425 of 427 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.2% (764 of 794 strings)

Translated using Weblate (Russian)

Currently translated at 94.1% (2969 of 3153 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3153 of 3153 strings)

Translated using Weblate (French)

Currently translated at 99.2% (788 of 794 strings)

Translated using Weblate (German)

Currently translated at 98.7% (232 of 235 strings)

Translated using Weblate (German)

Currently translated at 91.6% (2890 of 3153 strings)

Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Vasily Maslyukov <vasily.maslyukov@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: billy <kreideraine@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translation: Habitica/Achievements
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2024-08-28 12:49:00 +02:00
Phillip Thelen
4b1656f61c Correctly load bootstrap radio component (#15293) 2024-08-28 12:49:00 +02:00
Phillip Thelen
b42341e0ae better timezone handling 2024-08-28 12:49:00 +02:00
Phillip Thelen
267221544b fix loading user data in app.vue (#15292)
* fix handling user version

* fix notification display
2024-08-28 12:49:00 +02:00
Phillip Thelen
5ed9528241 load sounds from aws (#15249) 2024-08-28 12:49:00 +02:00
Phillip Thelen
2298f4cf7b Don’t load browser-script again if it’s already the correct language (#15263) 2024-08-28 12:49:00 +02:00
Phillip Thelen
b7cb743f14 Reduce size of client js bundles (#15264)
* add packages

* Only include the needed parts of BootstrapVue

* remove yargs from client

* treeshake validator library

* formatting

* fix import
2024-08-28 12:49:00 +02:00
Phillip Thelen
7f25232218 Refactor the root App to load less data for front page visits (#15265)
* refactor root app to not load everything when visiting landing page

# Conflicts:
#	website/client/src/app.vue

* fix lint

* fix hiding loading screen

* fix showing snackbars when not logged in

* remove console
2024-08-28 12:49:00 +02:00
Phillip Thelen
a7b9a78aa4 Improve rate limiting (#15272)
* Improve rate limiting

* make rate limiter config names more consistent

* fix tests and add new one

* correct math
2024-08-28 12:49:00 +02:00
Sabe Jones
6d7467ccaf Remove and clean up unused invite notif (#15279)
* fix(notifications): remove and clean up unused invite notif

* fix(lint): remove unused const

* refactor(invites): updateMany in migration, don't load inviter unless needed

* fix(lint): remove extra whitespace

* fix(groups): remove more broken inviter logic

---------

Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:49:00 +02:00
Phillip Thelen
b2531ecd0d Fix schedule using wrong month at the beginning hours of month (#15290)
* Fix schedule using wrong month at the beginning hours of month

* fix broken test

* fix switchover for time based matchers

* Fix scheduling issue related to timezones

* Fix end date creating issues
2024-08-28 12:49:00 +02:00
Sabe Jones
83ffa929d5 5.27.2 2024-08-28 12:49:00 +02:00
Weblate
1ad5932d3d Translated using Weblate (French)
Currently translated at 97.8% (184 of 188 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3153 of 3153 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.2% (181 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.8% (167 of 188 strings)

Translated using Weblate (French)

Currently translated at 100.0% (235 of 235 strings)

Translated using Weblate (French)

Currently translated at 99.8% (3148 of 3153 strings)

Translated using Weblate (Indonesian)

Currently translated at 87.7% (165 of 188 strings)

Translated using Weblate (French)

Currently translated at 99.1% (787 of 794 strings)

Translated using Weblate (German)

Currently translated at 95.3% (757 of 794 strings)

Translated using Weblate (German)

Currently translated at 99.4% (385 of 387 strings)

Translated using Weblate (German)

Currently translated at 100.0% (878 of 878 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 96.0% (763 of 794 strings)

Translated using Weblate (Portuguese)

Currently translated at 35.1% (66 of 188 strings)

Translated using Weblate (Portuguese)

Currently translated at 25.5% (48 of 188 strings)

Co-authored-by: Catarina Rocha <caticalhau312@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/content/de/
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/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
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/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translation: Habitica/Backgrounds
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2024-08-28 12:49:00 +02:00
Natalie
9f2bdaaf55 Update static navbar on mobile (#15289)
* update logo on static pages (mobile)

* remove typo
2024-08-28 12:49:00 +02:00
Phillip Thelen
33a696be4d Add a timeout to mongoldb connections (#15258)
* Add option to set a socket timeout for mongodb requests

* Handle mongodb timeouts better

* add default to config
2024-08-28 12:49:00 +02:00
Natalie
7aeec2e8eb Navigation Bar Visual Improvements (#15278)
* update privacy policy

* Fix serving memoized content

* add missing info to mystery item strings

* add missing string info to July mystery items

* fix food

* melior updates - loading screen & menu bar

* updates to currency tray spacing

* table styling

* one last table style

* add margin adjustment

* fix spacing

* clean up spacing styles

* remove table rounded corners

* correct faq tyops

* spacing updates

* fix web challenge instructions

* beach umbrella you are not shady except when you are

* fix mobile margin weirdness

* update wording on answers

* mobile spacing

* add chart showing details of item 1.4

* revert unrelated changes

* melior centering and icon spacing

* remove unnecessary files

* rejiggering spacing

* remove errant comma

* remove unnecessary files

* remove duplicated class

* remove duplicated class

* ...and more spacing

* notification bubble

* notification icon & currency alignment

* fix(CSS): clean up some unnecessary classes

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:49:00 +02:00
Phillip Thelen
187f6a2547 fix date dependatent time travel test (#15285) 2024-08-28 12:48:11 +02:00
Natalie
77a6335706 update armorArmoireBasketballUniformNotes to correct stat display (#15262)
Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:48:11 +02:00
Natalie
db913a1373 FAQ Typos (#15282)
* update privacy policy

* Fix serving memoized content

* add missing info to mystery item strings

* add missing string info to July mystery items

* fix food

* melior updates - loading screen & menu bar

* updates to currency tray spacing

* table styling

* one last table style

* add margin adjustment

* fix spacing

* clean up spacing styles

* remove table rounded corners

* correct faq tyops

* spacing updates

* fix web challenge instructions

* update wording on answers

* revert unrelated changes

---------

Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:48:11 +02:00
Sabe Jones
13b51dca74 5.27.1 2024-08-28 12:48:11 +02:00
Weblate
30da77022f Translated using Weblate (Portuguese)
Currently translated at 24.4% (46 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3085 of 3153 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.6% (109 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (3105 of 3153 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 79.2% (149 of 188 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.4% (44 of 188 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.4% (44 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 94.7% (752 of 794 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (387 of 387 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (184 of 193 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (878 of 878 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (878 of 878 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.2% (764 of 794 strings)

Translated using Weblate (Ukrainian)

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 (Indonesian)

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Co-authored-by: Catarina Rocha <caticalhau312@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Muhammad Hanafi <Cytricgame@gmail.com>
Co-authored-by: Márcio Ramos Corrêa <marcio.ramos.correa@gmail.com>
Co-authored-by: Pierre Huang <3541262043@qq.com>
Co-authored-by: Static Meteor <josusantaeufemiaiglesias@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Tasks
2024-08-28 12:48:11 +02:00
Sabe Jones
d2017c276d fix(lint): clean up migration for ongoing use 2024-08-28 12:48:11 +02:00
Sabe Jones
6a9d5a826c chore(migration): set up for Naming Day 2024 2024-08-28 12:48:11 +02:00
Phillip Thelen
f137c472da fix displaying if a pet is hatchable (#15281) 2024-08-28 12:48:03 +02:00
dependabot[bot]
c6086b958e Bump fast-xml-parser from 4.3.4 to 4.4.1 (#15280)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 4.3.4 to 4.4.1.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.3.4...v4.4.1)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
2024-08-28 12:48:03 +02:00
Sabe Jones
9d4a2a1437 fix(customization): restore broken popovers 2024-08-28 12:48:03 +02:00
Sabe Jones
dcddbcbda3 fix(lint): remove console statement 2024-08-28 12:48:03 +02:00
Sabe Jones
9891658da7 Squashed commit of the following:
commit 8ed95731cb
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jul 23 17:59:56 2024 +0200

    fix hatched dialog

commit 53242ad96c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jul 23 17:38:13 2024 +0200

    fix popover not showing

commit ce4bfd25bd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jul 3 17:28:30 2024 +0200

    move item popover to own component

commit 2e6a300c46
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jul 1 18:48:24 2024 +0200

    make scaled sprites look nice

commit a3cbadb8c2
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jul 1 18:48:17 2024 +0200

    fix hatching dialog

commit 0e5126df5e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jul 1 18:48:12 2024 +0200

    fix popover alignment

commit 7362af9236
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 28 17:07:03 2024 +0200

    fix item display

commit cf353efdb7
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 28 16:59:13 2024 +0200

    fix pet display

commit caf0cba9f2
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 28 15:24:39 2024 +0200

    fix background icon display

commit 3b06febc01
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 26 12:30:04 2024 +0200

    fix sprites for notifications

commit 160b2debdc
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 26 10:35:13 2024 +0200

    fix gear display in profile

commit b200a2f17d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 26 10:28:11 2024 +0200

    fix sprites for keys to the kennel

commit 3614e7a8fb
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 26 10:28:00 2024 +0200

    fix sprites on avatar customization

commit 35f993d055
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 26 09:18:41 2024 +0200

    fix hover icons

commit 28fc80115e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jun 11 16:50:37 2024 +0200

    remove console

commit b041c67679
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jun 11 15:18:44 2024 +0200

    more lint fixes

commit f4261d0440
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jun 11 15:18:16 2024 +0200

    fix lint

commit 878ee8f77b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jun 11 13:23:08 2024 +0200

    support gifs

commit aac24715aa
Author: Phillip Thelen <phillip@habitica.com>
Date:   Tue Jun 11 13:15:52 2024 +0200

    move avatar customization to sprites

commit f4d3663130
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 7 17:25:19 2024 +0200

    Move more sprites out of css

commit 6e6b4c981a
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 7 16:59:30 2024 +0200

    add new sprite to item and shopItem component

commit 8712413f5d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 7 16:37:24 2024 +0200

    use new sprites for pets list

commit 1172893826
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 5 09:43:51 2024 +0200

    Begin building new system for loading sprites
2024-08-28 12:48:03 +02:00
Phillip Thelen
f506044aa6 Seasonal gear fix (#15255)
* cleanup unneeded season definitions

* assign first winter seasonal gear right season

* add missing winter definition

* Fix enddate for winter galas

* fix lint

* fix halloween sprites

* set season

* fix loading habitoween sprites

* add missing customization shop sprites

* Fix test

* update customization shop sprites
2024-08-28 12:48:02 +02:00
Phillip Thelen
3cbb67c71a Steampunk gear should not have an end date (#15256) 2024-08-28 12:48:02 +02:00
Sabe Jones
d7ed938efc fix(lint): clean up migration for ongoing use 2024-08-28 12:48:02 +02:00
Sabe Jones
134401d153 chore(migration): set up for Naming Day 2024 2024-08-28 12:48:02 +02:00
Sabe Jones
fb29ee22f8 5.27.0 2024-08-28 12:48:02 +02:00
Weblate
bf5825f1a9 Translated using Weblate (French)
Currently translated at 99.7% (386 of 387 strings)

Translated using Weblate (French)

Currently translated at 100.0% (878 of 878 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (235 of 235 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3153 of 3153 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (794 of 794 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (387 of 387 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (876 of 878 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.9% (129 of 133 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.5% (3067 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 78.1% (147 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.2% (764 of 794 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (234 of 234 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (108 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (261 of 264 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.2% (3055 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 39.3% (74 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.2% (764 of 794 strings)

Translated using Weblate (Spanish)

Currently translated at 99.3% (789 of 794 strings)

Translated using Weblate (Indonesian)

Currently translated at 79.1% (72 of 91 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese)

Currently translated at 97.6% (163 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 96.8% (760 of 785 strings)

Co-authored-by: Alex Kalergi <nitrogenphospat@proton.me>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Muhammad Hanafi <Cytricgame@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
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/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-08-28 12:48:02 +02:00
Natalie
56b3a881db August 2024 Content Prebuild (#15277)
* add sprites

* 2024-08 content build

* fixing stuff

* update boss rage effects

* update quest rage text and releaseDates.js

* update shop-featuredItems.js

* fricken fracken beach umbrella

* more beach umbrella

* remove egregious typo
2024-08-28 12:48:02 +02:00
Sabe Jones
569b2fce2d 5.26.3 2024-08-28 12:48:02 +02:00
Weblate
29fe48bb17 Translated using Weblate (German)
Currently translated at 91.8% (2888 of 3143 strings)

Translated using Weblate (Slovak)

Currently translated at 97.2% (177 of 182 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (785 of 785 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Slovak)

Currently translated at 92.3% (12 of 13 strings)

Translated using Weblate (German)

Currently translated at 91.7% (2883 of 3143 strings)

Translated using Weblate (Slovak)

Currently translated at 96.7% (176 of 182 strings)

Translated using Weblate (Indonesian)

Currently translated at 33.5% (63 of 188 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 2.6% (5 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (765 of 785 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Indonesian)

Currently translated at 32.9% (62 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 29.2% (55 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (German)

Currently translated at 91.6% (2881 of 3143 strings)

Translated using Weblate (Slovak)

Currently translated at 93.9% (171 of 182 strings)

Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Turkish)

Currently translated at 80.4% (107 of 133 strings)

Translated using Weblate (Turkish)

Currently translated at 7.9% (15 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 95.6% (751 of 785 strings)

Translated using Weblate (French)

Currently translated at 98.3% (772 of 785 strings)

Translated using Weblate (German)

Currently translated at 91.6% (2879 of 3143 strings)

Translated using Weblate (French)

Currently translated at 98.0% (770 of 785 strings)

Co-authored-by: Adithya Mahadev. B <adithyamahadev2521@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Ece Ekinci <plutonium3613@gmail.com>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Npc
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Spells
Translation: Habitica/Tasks
2024-08-28 12:48:02 +02:00
Sabe Jones
399d406ebb feat(avatar): popovers for customization 2024-08-28 12:48:02 +02:00
Phillip Thelen
bacb015f62 fix purchasing wacky potions (#15276) 2024-08-28 12:48:02 +02:00
Phillip Thelen
e5cb82e20f Fix some scheduling issues (#15274)
* fix logic for time travelers schedule

* fix potion availability

* fix tests
2024-08-28 12:48:02 +02:00
Sabe Jones
04573ff422 5.26.2 2024-08-28 12:47:56 +02:00
Weblate
173ee9f929 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.6% (872 of 875 strings)

Translated using Weblate (German)

Currently translated at 91.5% (2877 of 3143 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Spanish)

Currently translated at 98.2% (771 of 785 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.9% (3046 of 3143 strings)

Translated using Weblate (Portuguese)

Currently translated at 57.0% (1793 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (765 of 785 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Italian)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Italian)

Currently translated at 86.1% (368 of 427 strings)

Translated using Weblate (Italian)

Currently translated at 11.1% (21 of 188 strings)

Translated using Weblate (Italian)

Currently translated at 94.2% (740 of 785 strings)

Translated using Weblate (Italian)

Currently translated at 54.9% (50 of 91 strings)

Translated using Weblate (Italian)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Italian)

Currently translated at 97.4% (188 of 193 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Italian)

Currently translated at 74.9% (194 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (German)

Currently translated at 91.4% (2875 of 3143 strings)

Translated using Weblate (Italian)

Currently translated at 94.6% (828 of 875 strings)

Translated using Weblate (Italian)

Currently translated at 94.0% (823 of 875 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (German)

Currently translated at 91.4% (2873 of 3143 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Czech)

Currently translated at 85.7% (114 of 133 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (German)

Currently translated at 91.3% (2871 of 3143 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (133 of 133 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (133 of 133 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.4% (21 of 22 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (German)

Currently translated at 91.2% (2869 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (133 of 133 strings)

Translated using Weblate (French)

Currently translated at 100.0% (133 of 133 strings)

Translated using Weblate (German)

Currently translated at 98.4% (131 of 133 strings)

Translated using Weblate (French)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (French)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3143 of 3143 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (239 of 240 strings)

Translated using Weblate (French)

Currently translated at 100.0% (240 of 240 strings)

Translated using Weblate (German)

Currently translated at 98.7% (237 of 240 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Slovak)

Currently translated at 97.9% (189 of 193 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (Hebrew)

Currently translated at 92.8% (155 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (781 of 785 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (259 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Portuguese)

Currently translated at 65.3% (153 of 234 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (261 of 264 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.9% (3046 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 76.5% (144 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 28.1% (53 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (781 of 785 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.1% (377 of 384 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese)

Currently translated at 97.6% (163 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (258 of 259 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (German)

Currently translated at 91.2% (2867 of 3143 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.4% (773 of 785 strings)

Translated using Weblate (French)

Currently translated at 100.0% (234 of 234 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (French)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 61.7% (1940 of 3143 strings)

Translated using Weblate (Portuguese)

Currently translated at 57.0% (1793 of 3143 strings)

Translated using Weblate (German)

Currently translated at 91.1% (2865 of 3143 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (3085 of 3102 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (234 of 234 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 61.7% (1940 of 3143 strings)

Translated using Weblate (German)

Currently translated at 91.0% (2863 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (234 of 234 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Slovak)

Currently translated at 74.9% (588 of 785 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 95.6% (109 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (427 of 427 strings)

Translated using Weblate (German)

Currently translated at 91.0% (2861 of 3143 strings)

Translated using Weblate (German)

Currently translated at 98.7% (231 of 234 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (German)

Currently translated at 99.2% (262 of 264 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (234 of 234 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (264 of 264 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3143 of 3143 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (286 of 286 strings)

Translated using Weblate (German)

Currently translated at 90.9% (2857 of 3143 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (382 of 384 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (382 of 384 strings)

Translated using Weblate (Slovak)

Currently translated at 97.6% (375 of 384 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (German)

Currently translated at 99.7% (383 of 384 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.8% (3043 of 3143 strings)

Translated using Weblate (German)

Currently translated at 90.8% (2855 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.7% (3041 of 3143 strings)

Translated using Weblate (French)

Currently translated at 99.6% (3132 of 3143 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (377 of 377 strings)

Translated using Weblate (French)

Currently translated at 100.0% (384 of 384 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.3% (799 of 875 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 98.9% (380 of 384 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.3% (184 of 193 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (377 of 378 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.0% (157 of 167 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (192 of 193 strings)

Translated using Weblate (French)

Currently translated at 99.0% (3114 of 3143 strings)

Translated using Weblate (German)

Currently translated at 90.7% (2853 of 3143 strings)

Translated using Weblate (French)

Currently translated at 100.0% (785 of 785 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.0% (157 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (German)

Currently translated at 98.5% (421 of 427 strings)

Translated using Weblate (French)

Currently translated at 98.7% (3105 of 3143 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (French)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (German)

Currently translated at 98.4% (190 of 193 strings)

Translated using Weblate (French)

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Turkish)

Currently translated at 5.8% (11 of 188 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (785 of 785 strings)

Translated using Weblate (German)

Currently translated at 98.4% (773 of 785 strings)

Translated using Weblate (German)

Currently translated at 65.9% (60 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (873 of 875 strings)

Translated using Weblate (German)

Currently translated at 100.0% (875 of 875 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3143 of 3143 strings)

Co-authored-by: Alex G <powerali18@gmail.com>
Co-authored-by: Antonio Spinelli <tonicospinelli@users.noreply.translate.habitica.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Elisa Bailo <bailoelisa5@gmail.com>
Co-authored-by: Eva <kornmann.eva@gmx.de>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: Isabela de Carvalho <isabela.c.escritora@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Justcallme rye <Blizzardscf32@gmail.com>
Co-authored-by: Miguel Rocha de Macedo Amaral <miguelrmamaral@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Paolo Bizzarri <pibizza@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Vinicius Rodrigues <suburbanizar@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yan <ariamao0802@gmail.com>
Co-authored-by: billypat <kreideraine@gmail.com>
Co-authored-by: diamondmonster09 <diamondmonster09@gmail.com>
Co-authored-by: tamara <tab.bravani@gmail.com>
Co-authored-by: 过客是个铁憨憨 <1811304592@qq.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/he/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/id/
Translate-URL: https://translate.habitica.com/projects/habitica/character/it/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/it/
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/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/it/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/it/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/id/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/es/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/it/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/merch/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/es/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/it/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/de/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/es/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/it/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/it/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
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/tasks/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/tr/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Merch
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/Subscriber
Translation: Habitica/Tasks
2024-08-28 12:47:56 +02:00
Phillip Thelen
cf31227df4 Update schedule.js (#15273)
Add SandSculpture for July
2024-08-28 12:47:56 +02:00
Sabe Jones
a5c2cc6c6a Update README.md
fix awkward line breaks
2024-08-28 12:47:56 +02:00
Sabe Jones
4d8380f285 Update README.md 2024-08-28 12:47:56 +02:00
Sabe Jones
5686ecdd16 fix(quests): adjust margins 2024-08-28 12:47:56 +02:00
Sabe Jones
7e8239fb28 fix(modals): various tweks 2024-08-28 12:47:56 +02:00
Sabe Jones
193f783652 fix(quests): modest Bundle revisions 2024-08-28 12:47:56 +02:00
Sabe Jones
a51230c847 fix(schedule): add Oddballs to November 2024-08-28 12:47:56 +02:00
Sabe Jones
bbc07a8abd fix(lint): add missing whitespace for curlies 2024-08-28 12:47:56 +02:00
Sabe Jones
2fb2af20c0 test(chat): coverage for new sunsets 2024-08-28 12:47:56 +02:00
Sabe Jones
ec6bb3ae79 fix(groups): fix TypeError when missing 2024-08-28 12:47:56 +02:00
Sabe Jones
a0520ddb3f fix(api): close lingering deprecated APIs 2024-08-28 12:47:56 +02:00
Sabe Jones
e89bf3e588 fix(memoize): enumerate food 2024-08-28 12:47:56 +02:00
Phillip Thelen
6195071730 remove console 2024-08-22 11:42:51 +02:00
Phillip Thelen
d872ba49fd fix email search 2024-08-22 11:33:52 +02:00
Phillip Thelen
cea9743087 Split email search into multiple queries 2024-08-21 13:16:22 +02:00
Phillip Thelen
b31a6453a3 fix sorting 2024-08-19 17:30:37 +02:00
Phillip Thelen
5fb1e250ee fix loading spinner 2024-08-19 17:19:31 +02:00
Phillip Thelen
baec8273d0 time travel button display fix 2024-08-19 16:15:22 +02:00
Phillip Thelen
e387585d6d search improvements 2024-08-19 16:15:14 +02:00
Phillip Thelen
2b328c37f7 remove autocannon file 2024-08-19 15:19:07 +02:00
Phillip Thelen
9224f58da5 admin panel display improvements 2024-08-19 15:13:32 +02:00
Phillip Thelen
4d64113613 Fix bottom button display 2024-07-30 18:16:25 +02:00
Phillip Thelen
0305ba4269 Unify date preview display 2024-07-30 18:16:14 +02:00
Phillip Thelen
e2f25e34e6 fix checkbox alignment in admin panel 2024-07-29 18:18:31 +02:00
Phillip Thelen
26d5a4503c improve sub styling 2024-07-29 16:12:35 +02:00
Phillip Thelen
abf5629f80 add styling for warning buttons 2024-07-29 16:12:22 +02:00
Phillip Thelen
174e2c0078 load own user when pressing enter on empty field 2024-07-26 14:47:52 +02:00
Phillip Thelen
b104e371ef escape regex for searching users 2024-07-25 17:15:58 +02:00
Phillip Thelen
6bde0e3fd9 fix width of items 2024-07-25 17:01:52 +02:00
Phillip Thelen
7f66cc28e0 fix test 2024-07-17 15:43:38 +02:00
Phillip Thelen
1a2f299e04 Make Admin panel design more consistent 2024-07-17 15:06:11 +02:00
Phillip Thelen
489bd851bb Add option to search for users by email or username in admin panel 2024-07-17 13:32:25 +02:00
Sabe Jones
04554c5309 5.26.1 2024-07-03 14:23:37 -05:00
Phillip Thelen
5ef88b5c56 Update releaseDates.js 2024-07-03 18:52:23 +02:00
Weblate
892c4934d5 Translated using Weblate (Japanese)
Currently translated at 99.2% (3080 of 3103 strings)

Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translation: Habitica/Gear
2024-07-01 22:51:54 +02:00
Sabe Jones
b90457c04f fix(test): correct hatching potion test 2024-06-28 11:20:21 -05:00
Sabe Jones
379d98a91e fix(test): correct schedule test 2024-06-28 11:09:09 -05:00
Sabe Jones
07352480cd Merge remote-tracking branch 'origin/phillip/memoize-me' into develop 2024-06-28 10:26:38 -05:00
Phillip Thelen
1fb44bbe73 fix naming 2024-06-28 10:20:11 -05:00
Phillip Thelen
5323849f90 fix naming 2024-06-28 17:16:45 +02:00
Sabe Jones
034327f647 5.26.0 2024-06-28 09:55:53 -05:00
Sabe Jones
de9aac0988 Squashed commit of the following:
commit 8309686922
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 25 15:41:41 2024 -0400

    melior updates - loading screen & menu bar

commit 53608dd688
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 24 22:12:09 2024 +0200

    fix food

commit eecae86454
Merge: 95c562fdbc 960e262f19
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:05:04 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 960e262f19
Merge: c5bbadaacd 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:40 2024 -0400

    Merge branch '2024-07-content-prebuild' of https://github.com/HabitRPG/habitica-private into 2024-07-content-prebuild

commit c5bbadaacd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:23 2024 -0400

    add missing string info to July mystery items

commit 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 11:29:17 2024 -0400

    add missing info to mystery item strings

commit 95c562fdbc
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 21 11:12:18 2024 +0200

    Fix serving memoized content

commit 877fe48225
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 12:23:24 2024 +0200

    correctly memoize conent api

commit e0f6f79c5b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 10:11:27 2024 +0200

    don’t build multiple times on heroku

commit f62254d68e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:40:20 2024 +0200

    fix client command

commit d054e6fc16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:36:57 2024 +0200

    correct build call

commit 7231f699c1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:32:32 2024 +0200

    try setting up with heroku buildpack

commit 1dae0793fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:50:32 2024 +0200

    call gulp build:prod

commit f18fbe86b6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:40:53 2024 +0200

    build client

commit 61a61724ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:33:18 2024 +0200

    testing

commit 93cf30eb18
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:20:25 2024 +0200

    integration fix

commit cff08adcd0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:13:20 2024 +0200

    specify dev docker file

commit 4da2ed4a1f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:10:07 2024 +0200

    initialize stub

commit 11c5b26c59
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:08:45 2024 +0200

    test heroku file

commit ac85bb2e2d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:03:15 2024 +0200

    fix stub reference

commit 74dfb2710f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:01:27 2024 +0200

    test fixes

commit 8dbd3c3db1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:37:04 2024 +0200

    fix canOwn test

commit 74b3b348ff
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:32:31 2024 +0200

    fix buy test

commit 3386d61fde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:30:37 2024 +0200

    fix debug tests

commit 19da14531c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:25 2024 +0200

    add chameleon to featured quests

commit 254dd80f24
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:14 2024 +0200

    fix import

commit 0bc3f16b4b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:22 2024 +0200

    add new content to new release file

commit 5184973bd5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:11 2024 +0200

    fix release date tests

commit b6accca5ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:06 2024 +0200

    fix armoire tests

commit fec68e6211
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:02:03 2024 +0200

    fix tests

commit fc63c906dd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:44:21 2024 +0200

    Improve test coverage

commit 3333f8f0f5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:24:59 2024 +0200

    allow hatching potions to have a release date

commit 89a3ac3dde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:11:38 2024 +0200

    allow eggs to have a release date

    # Conflicts:
    #	test/content/armoire.test.js

commit 16551ec83f
Merge: f5f4974a73 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:03:12 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit f5f4974a73
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 14:58:13 2024 -0400

    update habitica-images

commit 162e337d14
Merge: f2506c3231 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:46:03 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit f2506c3231
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:24:21 2024 -0400

    updated sprites css

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:55:41 -05:00
Sabe Jones
f55d836398 Squashed commit of the following:
commit 960e262f19
Merge: c5bbadaacd 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:40 2024 -0400

    Merge branch '2024-07-content-prebuild' of https://github.com/HabitRPG/habitica-private into 2024-07-content-prebuild

commit c5bbadaacd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 16:03:23 2024 -0400

    add missing string info to July mystery items

commit 7ec8b84b01
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 24 11:29:17 2024 -0400

    add missing info to mystery item strings

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:50:28 -05:00
Sabe Jones
287014518d Squashed commit of the following:
commit 28193f86fb
Author: Phillip Thelen <phillip@habitica.com>
Date:   Fri Jun 21 11:12:18 2024 +0200

    Fix serving memoized content

commit 877fe48225
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 12:23:24 2024 +0200

    correctly memoize conent api

commit e0f6f79c5b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Thu Jun 20 10:11:27 2024 +0200

    don’t build multiple times on heroku

commit f62254d68e
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:40:20 2024 +0200

    fix client command

commit d054e6fc16
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:36:57 2024 +0200

    correct build call

commit 7231f699c1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 19:32:32 2024 +0200

    try setting up with heroku buildpack

commit 1dae0793fd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:50:32 2024 +0200

    call gulp build:prod

commit f18fbe86b6
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:40:53 2024 +0200

    build client

commit 61a61724ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:33:18 2024 +0200

    testing

commit 93cf30eb18
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:20:25 2024 +0200

    integration fix

commit cff08adcd0
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:13:20 2024 +0200

    specify dev docker file

commit 4da2ed4a1f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:10:07 2024 +0200

    initialize stub

commit 11c5b26c59
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:08:45 2024 +0200

    test heroku file

commit ac85bb2e2d
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:03:15 2024 +0200

    fix stub reference

commit 74dfb2710f
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 18:01:27 2024 +0200

    test fixes

commit 8dbd3c3db1
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:37:04 2024 +0200

    fix canOwn test

commit 74b3b348ff
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:32:31 2024 +0200

    fix buy test

commit 3386d61fde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:30:37 2024 +0200

    fix debug tests

commit 19da14531c
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:25 2024 +0200

    add chameleon to featured quests

commit 254dd80f24
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 17:05:14 2024 +0200

    fix import

commit 0bc3f16b4b
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:22 2024 +0200

    add new content to new release file

commit 5184973bd5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:11 2024 +0200

    fix release date tests

commit b6accca5ca
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:33:06 2024 +0200

    fix armoire tests

commit fec68e6211
Author: Phillip Thelen <phillip@habitica.com>
Date:   Wed Jun 19 16:02:03 2024 +0200

    fix tests

commit fc63c906dd
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:44:21 2024 +0200

    Improve test coverage

commit 3333f8f0f5
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:24:59 2024 +0200

    allow hatching potions to have a release date

commit 89a3ac3dde
Author: Phillip Thelen <phillip@habitica.com>
Date:   Mon Jun 10 14:11:38 2024 +0200

    allow eggs to have a release date

    # Conflicts:
    #	test/content/armoire.test.js

commit 16551ec83f
Merge: f5f4974a73 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:03:12 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 2645bf6023
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 15:02:47 2024 -0400

    update habitica images

commit f5f4974a73
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 14:58:13 2024 -0400

    update habitica-images

commit 162e337d14
Merge: f2506c3231 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:46:03 2024 -0400

    Merge branch '2024-07-content-prebuild' into subs-private

commit 21a7d36b7b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:45:09 2024 -0400

    update sprites

commit f2506c3231
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 13:24:21 2024 -0400

    updated sprites css

commit d47641e25a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 18 12:46:59 2024 -0400

    typo fix

commit fb8479ad1e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 17 13:44:36 2024 -0400

    finish July prebuild

commit 3810cf3ef3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 14 10:42:47 2024 -0400

    add chameleon quest

commit d05da3722c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 17:12:43 2024 -0400

    add June background notes

commit b8a3440ef2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 16:40:04 2024 -0400

    fix mystery item and background description

commit 44d63032d8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 15:38:23 2024 -0400

    add subscriber gear, enchanted armoire, and background

commit 9d7da91ec6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 13 14:44:59 2024 -0400

    add sprites
2024-06-28 09:49:08 -05:00
Weblate
b46e2da61b Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2024-06-28 16:41:20 +02:00
Emilia Skoglund
ef47d6cf0b Translated using Weblate (Swedish)
Currently translated at 80.0% (619 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sv/
2024-06-28 05:28:46 +02:00
Daniel Faria Gomes
1f5d66cd58 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (773 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
2024-06-28 05:28:46 +02:00
Natalie Luhrs
a88602a21f Translated using Weblate (Japanese)
Currently translated at 100.0% (773 of 773 strings)

Translation: Habitica/Questscontent
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
2024-06-27 20:24:32 +02:00
Sabe Jones
760c05df5d 5.25.8 2024-06-26 12:36:32 -05:00
Phillip Thelen
26d070f2c3 improve loggly request logging call (#15259) 2024-06-26 12:30:13 -05:00
Petal Forrest
bc9577439e Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (286 of 286 strings)

Translation: Habitica/Limited
Translate-URL: https://translate.habitica.com/projects/habitica/limited/en_GB/
2024-06-22 10:40:51 +02:00
Petal Forrest
10cd596f0b Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (427 of 427 strings)

Translation: Habitica/Groups
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
2024-06-22 10:40:50 +02:00
Toro Mor
d180062ad2 Translated using Weblate (German)
Currently translated at 91.9% (2852 of 3103 strings)

Translation: Habitica/Gear
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
2024-06-22 10:40:50 +02:00
Petal Forrest
bfacf4b36e Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (259 of 259 strings)

Translation: Habitica/Settings
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
2024-06-22 10:40:50 +02:00
Weblate
2912f31dec Translated using Weblate (English (United Kingdom))
Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (233 of 233 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 76.1% (325 of 427 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (378 of 378 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (868 of 868 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (184 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 97.3% (184 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 99.8% (772 of 773 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (110 of 110 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 79.6% (691 of 868 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (167 of 167 strings)

Co-authored-by: Petal Forrest <adrijanaskar2008backup@gmail.com>
Co-authored-by: Razi H <razi.haj@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/content/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/en_GB/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Front
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2024-06-22 01:12:00 +02:00
Sabe Jones
c47b287a89 5.25.7 2024-06-21 13:28:34 -05:00
Sabe Jones
3aa626d2ae fix(sprites): remove potion PNG override 2024-06-21 13:28:29 -05:00
Phillip Thelen
28193f86fb Fix serving memoized content 2024-06-21 11:12:18 +02:00
Phillip Thelen
877fe48225 correctly memoize conent api 2024-06-20 12:23:24 +02:00
Phillip Thelen
e0f6f79c5b don’t build multiple times on heroku 2024-06-20 10:11:27 +02:00
Phillip Thelen
f62254d68e fix client command 2024-06-19 19:40:20 +02:00
Phillip Thelen
d054e6fc16 correct build call 2024-06-19 19:36:57 +02:00
Phillip Thelen
7231f699c1 try setting up with heroku buildpack 2024-06-19 19:32:32 +02:00
Phillip Thelen
1dae0793fd call gulp build:prod 2024-06-19 18:50:32 +02:00
Phillip Thelen
f18fbe86b6 build client 2024-06-19 18:40:53 +02:00
Phillip Thelen
61a61724ca testing 2024-06-19 18:33:18 +02:00
Phillip Thelen
93cf30eb18 integration fix 2024-06-19 18:20:43 +02:00
Phillip Thelen
cff08adcd0 specify dev docker file 2024-06-19 18:13:20 +02:00
Phillip Thelen
4da2ed4a1f initialize stub 2024-06-19 18:10:07 +02:00
Phillip Thelen
11c5b26c59 test heroku file 2024-06-19 18:08:45 +02:00
Phillip Thelen
ac85bb2e2d fix stub reference 2024-06-19 18:03:15 +02:00
Phillip Thelen
74dfb2710f test fixes 2024-06-19 18:02:28 +02:00
Phillip Thelen
8dbd3c3db1 fix canOwn test 2024-06-19 17:37:04 +02:00
Phillip Thelen
74b3b348ff fix buy test 2024-06-19 17:32:31 +02:00
Phillip Thelen
3386d61fde fix debug tests 2024-06-19 17:30:52 +02:00
Phillip Thelen
19da14531c add chameleon to featured quests 2024-06-19 17:05:25 +02:00
Phillip Thelen
254dd80f24 fix import 2024-06-19 17:05:14 +02:00
Phillip Thelen
0bc3f16b4b add new content to new release file 2024-06-19 16:33:22 +02:00
Phillip Thelen
5184973bd5 fix release date tests 2024-06-19 16:33:11 +02:00
Phillip Thelen
b6accca5ca fix armoire tests 2024-06-19 16:33:06 +02:00
Phillip Thelen
fec68e6211 fix tests 2024-06-19 16:02:03 +02:00
Phillip Thelen
fc63c906dd Improve test coverage 2024-06-19 15:25:45 +02:00
Phillip Thelen
3333f8f0f5 allow hatching potions to have a release date 2024-06-19 15:25:45 +02:00
Phillip Thelen
89a3ac3dde allow eggs to have a release date
# Conflicts:
#	test/content/armoire.test.js
2024-06-19 15:24:21 +02:00
CuriousMagpie
16551ec83f Merge branch '2024-07-content-prebuild' into subs-private 2024-06-18 15:03:12 -04:00
CuriousMagpie
2645bf6023 update habitica images 2024-06-18 15:02:47 -04:00
CuriousMagpie
f5f4974a73 update habitica-images 2024-06-18 14:58:13 -04:00
CuriousMagpie
162e337d14 Merge branch '2024-07-content-prebuild' into subs-private 2024-06-18 13:46:03 -04:00
CuriousMagpie
21a7d36b7b update sprites 2024-06-18 13:45:09 -04:00
CuriousMagpie
f2506c3231 updated sprites css 2024-06-18 13:24:21 -04:00
CuriousMagpie
d47641e25a typo fix 2024-06-18 12:46:59 -04:00
CuriousMagpie
fb8479ad1e finish July prebuild 2024-06-17 13:44:36 -04:00
CuriousMagpie
3810cf3ef3 add chameleon quest 2024-06-14 10:42:47 -04:00
CuriousMagpie
d05da3722c add June background notes 2024-06-13 17:12:43 -04:00
CuriousMagpie
b8a3440ef2 fix mystery item and background description 2024-06-13 16:40:04 -04:00
CuriousMagpie
44d63032d8 add subscriber gear, enchanted armoire, and background 2024-06-13 15:38:23 -04:00
CuriousMagpie
9d7da91ec6 add sprites 2024-06-13 14:44:59 -04:00
350 changed files with 9109 additions and 20188 deletions

View File

@@ -19,7 +19,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -41,7 +42,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -63,7 +65,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -86,7 +89,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -108,7 +112,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -137,7 +142,8 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -167,7 +173,8 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -197,7 +204,8 @@ jobs:
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -222,7 +230,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |
@@ -246,7 +255,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: sudo apt-get -y install libkrb5-dev
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
- name: npm install
run: |

25
.heroku/report_deploy.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
DEVELOPER="someone"
if git rev-parse --git-dir > /dev/null 2>&1; then
DEVELOPERS=$(git log -5 --pretty=format:'%an')
IFS=$'\n'
DEVELOPER=""
for dev in $DEVELOPERS
do
if [ "$DEVELOPER" == "someone" ]; then
if [[ ${dev} != *"[bot]"* ]]; then
DEVELOPER=$dev
continue
fi
continue
fi
done
fi
PARTS=$(cut -d"." -f1 <<< $BASE_URL)
SERVER_NAME=$(cut -d"/" -f3 <<< ${PARTS[0]})
SERVER_NAME=":$SERVER_EMOJI: $SERVER_NAME"
wget $SLACK_DEPLOY_URL --post-data="{\"server_name\": \"$SERVER_NAME\", \"developer\": \"$DEVELOPER\", \"base_url\": \"$BASE_URL\"}" -O /dev/null

View File

@@ -3,10 +3,13 @@ FROM node:20
# Install global packages
RUN npm install -g gulp-cli mocha
# Copy package.json and package-lock.json into image, then install
# dependencies.
# Copy package.json and package-lock.json into image
WORKDIR /usr/src/habitica
COPY ["package.json", "package-lock.json", "./"]
# Copy the remaining source files in.
COPY . /usr/src/habitica
# Install dependencies
RUN npm install
RUN npm run postinstall
RUN npm run client:build
RUN gulp build:prod

View File

@@ -1,14 +1,20 @@
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica ![Build Status](https://github.com/HabitRPG/habitica/workflows/Test/badge.svg)
===============
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn money to buy weapons and armor.
[Habitica](https://habitica.com) is an open-source habit-building program that treats your life like a role-playing game. Level up as you succeed, lose HP as you fail, and earn Gold to buy weapons and armor!
**We need more programmers!** Your assistance will be greatly appreciated. The wiki pages below and the additional pages they link to will tell you how to get started on contributing code and where you can go to seek further help or ask questions:
**Want to contribute code to Habitica?** We're always looking for assistance on any issues in our repo with the "Help Wanted" label. The wiki pages below and the additional linked pages will tell you how to start contributing code and where you can seek further help or ask questions:
* [Guidance for Blacksmiths](https://habitica.fandom.com/wiki/Guidance_for_Blacksmiths) - an introduction to the technologies used and how the software is organized.
* [Setting up Habitica Locally](https://habitica.fandom.com/wiki/Setting_up_Habitica_Locally) - how to set up a local install of Habitica for development and testing on various platforms.
* [Setting up Habitica Locally](https://github.com/HabitRPG/habitica/wiki/Setting-Up-Habitica-for-Local-Development) - how to set up a local install of Habitica for development and testing.
**Interested in contributing to Habiticas mobile apps?** Visit the links below for our mobile repositories.
* **Android:** https://github.com/HabitRPG/habitica-android
* **iOS:** https://github.com/HabitRPG/habitica-ios
Habitica's code is licensed as described at https://github.com/HabitRPG/habitica/blob/develop/LICENSE
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than creating an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Found a bug?** Please report it to [admin email](mailto:admin@habitica.com) rather than create an issue (an admin will advise you if a new issue is necessary; usually it is not).
**Have any questions about Habitica or its community?** See the links in the [habitica.com](https://habitica.com) website's Help menu or drop in to [Guilds > Tavern Chat](https://habitica.com/groups/tavern) to ask questions or chat socially!
**Creating a third-party tool?** Please review our [API Usage Guidelines](https://github.com/HabitRPG/habitica/wiki/API-Usage-Guidelines) to ensure that your tool is compliant and maintains the best experience for Habitica players.
**Have any questions about Habitica or contributing?** See the links in the [Habitica](https://habitica.com) website's Help menu. Theres FAQs, guides, and the option to reach out to us with any further questions!

View File

@@ -37,6 +37,7 @@
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",

View File

@@ -42,10 +42,41 @@ function cssVarMap (sprite) {
}
}
function createSpritesStream (name, src) {
function filterFile (file) {
if (file.relative.indexOf('Mount_Icon_') !== -1) {
return false;
}
if (file.path.indexOf('shop/') !== -1) {
return false;
}
if (file.path.indexOf('stable/eggs') !== -1) {
return false;
}
if (file.path.indexOf('stable/food') !== -1) {
return false;
}
if (file.path.indexOf('stable/potions') !== -1) {
return false;
}
if (file.relative.indexOf('shop_') === 0) {
return false;
}
if (file.relative.indexOf('icon_background') === 0) {
return false;
}
return true;
}
async function createSpritesStream (name, src) {
const stream = mergeStream();
// need to import this way bc of weird dependency things
// eslint-disable-next-line global-require
const filter = require('gulp-filter');
const f = filter(filterFile);
const spriteData = gulp.src(src)
.pipe(f)
.pipe(spritesmith({
imgName: `spritesmith-${name}.png`,
cssName: `spritesmith-${name}.css`,
@@ -63,7 +94,7 @@ function createSpritesStream (name, src) {
return stream;
}
gulp.task('sprites:main', () => {
gulp.task('sprites:main', async () => {
const mainSrc = sync('habitica-images/**/*.png');
return createSpritesStream('main', mainSrc);
});

View File

@@ -0,0 +1,47 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '2024_purge_invite_accepted';
const progressCount = 1000;
let count = 0;
async function updateUsers (userIds) {
count += userIds.length;
if (count % progressCount === 0) console.warn(`${count} ${userIds[0]}`);
return await User.updateMany(
{ _id: { $in: userIds } },
{ $pull: { notifications: { type: 'GROUP_INVITE_ACCEPTED' } } },
).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'notifications.type': 'GROUP_INVITE_ACCEPTED',
'auth.timestamps.loggedin': { $gt: new Date('2024-06-25') },
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({ _id: 1 })
.select({ _id: 1 })
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
const userIds = users.map(user => user._id);
await updateUsers(userIds); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,14 +1,12 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20240731_naming_day';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
count += 1;
let set;
let push;
@@ -115,16 +113,16 @@ async function updateUser (user) {
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
return user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
}
return user.updateOne({ $set: set, $inc: inc }).exec();
}
export default async function processUsers () {
let query = {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
'auth.timestamps.loggedin': { $gt: new Date('2024-07-01') },
};
const fields = {
@@ -136,7 +134,7 @@ export default async function processUsers () {
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.sort({ _id: 1 })
.select(fields)
.exec();
@@ -152,4 +150,4 @@ export default async function processUsers () {
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
}

906
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.25.6",
"version": "5.27.3",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -15,6 +15,7 @@
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"babel-preset-env": "^1.7.0",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.2",
@@ -35,6 +36,7 @@
"got": "^11.8.6",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"gulp.spritesmith": "^6.13.0",
@@ -74,6 +76,7 @@
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
@@ -107,7 +110,8 @@
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 5.0.23 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc"
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
},
"devDependencies": {
"axios": "^1.4.0",

View File

@@ -55,7 +55,7 @@ describe('contentLib', () => {
beforeEach(() => {
resSpy = generateRes();
if (fs.existsSync(contentLib.CONTENT_CACHE_PATH)) {
fs.rmdirSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
fs.rmSync(contentLib.CONTENT_CACHE_PATH, { recursive: true });
}
fs.mkdirSync(contentLib.CONTENT_CACHE_PATH);
});

View File

@@ -54,6 +54,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -71,6 +72,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when an unknown error is thrown by the rate limiter', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
sandbox.stub(logger, 'error');
sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.')));
@@ -104,6 +106,7 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is incorrect', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = 'das';
@@ -120,6 +123,7 @@ describe('rateLimiter middleware', () => {
it('limits when LIVELINESS_PROBE_KEY is not set', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
await attachRateLimiter(req, res, next);
@@ -135,6 +139,7 @@ describe('rateLimiter middleware', () => {
it('throws when LIVELINESS_PROBE_KEY is blank', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.query.liveliness = '';
@@ -150,6 +155,7 @@ describe('rateLimiter middleware', () => {
it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
// call for 31 times
@@ -173,6 +179,7 @@ describe('rateLimiter middleware', () => {
it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
@@ -199,4 +206,51 @@ describe('rateLimiter middleware', () => {
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.path = '/api/v4/user/auth/local/register';
req.ip = 1;
await attachRateLimiter(req, res, next);
req.headers['x-api-user'] = 'user-1';
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
// user id an ip are counted as separate sources
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 27, // 2 calls with user id
'X-RateLimit-Reset': sinon.match(Date),
});
req.headers['x-api-user'] = undefined;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 24, // 3 calls with only ip
'X-RateLimit-Reset': sinon.match(Date),
});
});
it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
req.ip = 1;
await attachRateLimiter(req, res, next);
await attachRateLimiter(req, res, next);
expect(res.set).to.have.been.calledWithMatch({
'X-RateLimit-Limit': 30,
'X-RateLimit-Remaining': 10,
'X-RateLimit-Reset': sinon.match(Date),
});
});
});

View File

@@ -3,6 +3,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('GET /groups/:groupId/chat', () => {
let user;
@@ -37,4 +38,34 @@ describe('GET /groups/:groupId/chat', () => {
});
});
});
context('public Guild', () => {
let group;
before(async () => {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
chat: [
'Hello',
'Welcome to the Guild',
],
}));
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
});
it('returns error if user attempts to fetch a sunset Guild', async () => {
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
});
});

View File

@@ -4,6 +4,7 @@ import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
describe('POST /chat/:chatId/like', () => {
let user;
@@ -111,4 +112,18 @@ describe('POST /chat/:chatId/like', () => {
message: t('groupNotFound'),
});
});
it('does not like a message that belongs to a sunset public group', async () => {
const message = await anotherUser.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
// Creation API is shut down, we need to simulate an extant public group
await Group.updateOne({ _id: groupWithChat._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('featureRetired'),
});
});
});

View File

@@ -0,0 +1,73 @@
import nconf from 'nconf';
import {
generateUser,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('POST /debug/boss-rage', () => {
let user;
let nconfStub;
beforeEach(async () => {
user = await generateUser();
});
beforeEach(() => {
nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('DEBUG_ENABLED').returns(true);
nconfStub.withArgs('BASE_URL').returns('https://example.com');
});
afterEach(() => {
nconfStub.restore();
});
it('errors if user is not in a party', async () => {
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 400,
error: 'BadRequest',
message: 'User not in a party.',
});
});
it('returns error when not in production mode', async () => {
nconfStub.withArgs('DEBUG_ENABLED').returns(false);
await expect(user.post('/debug/boss-rage'))
.to.eventually.be.rejected.and.deep.equal({
code: 404,
error: 'NotFound',
message: 'Not found.',
});
});
context('user is in a party', async () => {
let party;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
},
members: 2,
});
party = group;
user = groupLeader;
});
it('increases boss rage to 50', async () => {
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(50);
});
it('increases boss rage to 100', async () => {
await user.post('/debug/boss-rage');
await user.post('/debug/boss-rage');
await party.sync();
expect(party.quest.progress.rage).to.eql(100);
});
});
});

View File

@@ -34,9 +34,11 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: 1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() + 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
const tomorrow = new Date(today.valueOf());
tomorrow.setDate(today.getDate() + 1);
expect(newResultDate.getDate()).to.eql(tomorrow.getDate());
expect(newResultDate.getMonth()).to.eql(tomorrow.getMonth());
expect(newResultDate.getFullYear()).to.eql(tomorrow.getFullYear());
});
it('jumps back', async () => {
@@ -45,9 +47,11 @@ describe('POST /debug/jump-time', () => {
expect(resultDate.getMonth()).to.eql(today.getMonth());
expect(resultDate.getFullYear()).to.eql(today.getFullYear());
const newResultDate = new Date((await user.post('/debug/jump-time', { offsetDays: -1 })).time);
expect(newResultDate.getDate()).to.eql(today.getDate() - 1);
expect(newResultDate.getMonth()).to.eql(today.getMonth());
expect(newResultDate.getFullYear()).to.eql(today.getFullYear());
const yesterday = new Date(today.valueOf());
yesterday.setDate(today.getDate() - 1);
expect(newResultDate.getDate()).to.eql(yesterday.getDate());
expect(newResultDate.getMonth()).to.eql(yesterday.getMonth());
expect(newResultDate.getFullYear()).to.eql(yesterday.getFullYear());
});
it('can jump a lot', async () => {

View File

@@ -85,22 +85,6 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 1);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: guild.name,
}),
};
expect(inviter.notifications[1].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[1].data).to.eql(expectedData);
});
it('awards Joined Guild achievement', async () => {
await invitedUser.post(`/groups/${guild._id}/join`);
@@ -155,23 +139,6 @@ describe('POST /group/:groupId/join', () => {
await expect(invitedUser.get('/user')).to.eventually.have.nested.property('party._id', party._id);
});
it('notifies inviting user that their invitation was accepted', async () => {
await invitedUser.post(`/groups/${party._id}/join`);
const inviter = await user.get('/user');
const expectedData = {
headerText: t('invitationAcceptedHeader'),
bodyText: t('invitationAcceptedBody', {
username: invitedUser.auth.local.username,
groupName: party.name,
}),
};
expect(inviter.notifications[0].type).to.eql('GROUP_INVITE_ACCEPTED');
expect(inviter.notifications[0].data).to.eql(expectedData);
});
it('clears invitation from user when joining party', async () => {
await invitedUser.post(`/groups/${party._id}/join`);

View File

@@ -10,6 +10,7 @@ describe('GET /heroes/:heroId', () => {
const heroFields = [
'_id', 'id', 'auth', 'balance', 'contributor', 'flags', 'items',
'lastCron', 'party', 'preferences', 'profile', 'purchased', 'secret', 'achievements',
'stats',
];
before(async () => {

View File

@@ -11,6 +11,7 @@ describe('PUT /heroes/:heroId', () => {
const heroFields = [
'_id', 'auth', 'balance', 'contributor', 'flags', 'items', 'lastCron',
'party', 'preferences', 'profile', 'purchased', 'secret', 'permissions', 'achievements',
'stats',
];
before(async () => {

View File

@@ -123,7 +123,7 @@ describe('GET /world-state', () => {
const res = await requester().get('/world-state');
expect(res.npcImageSuffix).to.equal('winter');
expect(res.npcImageSuffix).to.equal('fall');
});
});
});

View File

@@ -47,15 +47,17 @@ describe('shops', () => {
describe('premium hatching potions', () => {
it('contains current scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
expect(potions.items.length).to.eql(3);
});
it('does not contain past scheduled premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length).to.eql(0);
expect(potions.items.filter(x => x.key === 'Aquatic' || x.key === 'Celestial').length, 'Aquatic or Celestial found').to.eql(0);
});
it('returns end date for scheduled premium potions', async () => {
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
potions.items.forEach(potion => {
@@ -73,9 +75,9 @@ describe('shops', () => {
});
it('does not contain locked quest premium hatching potions', async () => {
clock = sinon.useFakeTimers(new Date('2024-04-01'));
clock = sinon.useFakeTimers(new Date('2024-04-01T09:00:00.000Z'));
const potions = shared.shops.getMarketCategories(user).find(x => x.identifier === 'premiumHatchingPotions');
expect(potions.items.length).to.eql(2);
expect(potions.items.length).to.eql(3);
expect(potions.items.filter(x => x.key === 'Bronze' || x.key === 'BlackPearl').length).to.eql(0);
});
@@ -341,6 +343,16 @@ describe('shops', () => {
const backgrounds = shopCategories.find(cat => cat.identifier === 'backgrounds').items;
expect(backgrounds.length).to.be.greaterThan(0);
});
it('does not add an end date to steampunk gear', () => {
const categories = shopCategories.filter(cat => cat.identifier.startsWith('30'));
categories.forEach(category => {
expect(category.end).to.not.exist;
category.items.forEach(item => {
expect(item.end).to.not.exist;
});
});
});
});
describe('customizationShop', () => {

View File

@@ -3,6 +3,7 @@ import armoireSet from '../../../website/common/script/content/gear/sets/armoire
describe('armoireSet items', () => {
it('checks if canOwn has the same id', () => {
Object.keys(armoireSet).forEach(type => {
if (type === 'all') return;
Object.keys(armoireSet[type]).forEach(itemKey => {
const ownedKey = `${type}_armoire_${itemKey}`;
expect(armoireSet[type][itemKey].canOwn({

View File

@@ -233,6 +233,17 @@ describe('shared.ops.purchase', () => {
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases event hatching potion', async () => {
clock.restore();
clock = sandbox.useFakeTimers(moment('2022-04-10').valueOf());
const type = 'hatchingPotions';
const key = 'Veggie';
await purchase(user, { params: { type, key } });
expect(user.items.hatchingPotions[key]).to.eql(1);
});
it('purchases hatching potion if user completed quest', async () => {
const type = 'hatchingPotions';
const key = 'Bronze';

View File

@@ -3,38 +3,26 @@ import forEach from 'lodash/forEach';
import {
expectValidTranslationString,
} from '../helpers/content.helper';
function makeArmoireIitemList () {
const armoire = require('../../website/common/script/content/gear/sets/armoire').default;
const items = [];
items.push(...Object.values(armoire.armor));
items.push(...Object.values(armoire.body));
items.push(...Object.values(armoire.eyewear));
items.push(...Object.values(armoire.head));
items.push(...Object.values(armoire.headAccessory));
items.push(...Object.values(armoire.shield));
items.push(...Object.values(armoire.weapon));
return items;
}
import armoire from '../../website/common/script/content/gear/sets/armoire';
describe('armoire', () => {
let clock;
beforeEach(() => {
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
});
afterEach(() => {
clock.restore();
if (clock) {
clock.restore();
}
});
it('does not return unreleased gear', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-02'));
const items = makeArmoireIitemList();
const items = armoire.all;
expect(items.length).to.equal(377);
expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty;
});
it('released gear has all required properties', async () => {
clock = sinon.useFakeTimers(new Date('2024-05-08'));
const items = makeArmoireIitemList();
const items = armoire.all;
expect(items.length).to.equal(396);
forEach(items, item => {
if (item.set !== undefined) {
@@ -48,29 +36,30 @@ describe('armoire', () => {
it('releases gear when appropriate', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z'));
const items = makeArmoireIitemList();
const items = armoire.all;
expect(items.length).to.equal(377);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-01-08'));
const januaryItems = makeArmoireIitemList();
const januaryItems = armoire.all;
expect(januaryItems.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07'));
const januaryItems2 = makeArmoireIitemList();
const januaryItems2 = armoire.all;
expect(januaryItems2.length).to.equal(381);
clock.restore();
delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')];
clock = sinon.useFakeTimers(new Date('2024-02-07T16:00:00.000Z'));
const febuaryItems = makeArmoireIitemList();
clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z'));
const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384);
});
it('sets have at least 2 items', () => {
const armoire = makeArmoireIitemList();
const setMap = {};
forEach(armoire, item => {
forEach(armoire.all, item => {
// Gotta have one outlier
if (!item.set || item.set.startsWith('armoire-')) return;
if (setMap[item.set] === undefined) {
setMap[item.set] = 0;
}

View File

@@ -5,29 +5,51 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import * as eggs from '../../website/common/script/content/eggs';
import eggs from '../../website/common/script/content/eggs';
describe('eggs', () => {
describe('all', () => {
it('is a combination of drop and quest eggs', () => {
const dropNumber = Object.keys(eggs.drops).length;
const questNumber = Object.keys(eggs.quests).length;
const allNumber = Object.keys(eggs.all).length;
let clock;
expect(allNumber).to.be.greaterThan(0);
expect(allNumber).to.equal(dropNumber + questNumber);
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('contains basic information about each egg', () => {
each(eggs.all, (egg, key) => {
expectValidTranslationString(egg.text);
expectValidTranslationString(egg.adjective);
expectValidTranslationString(egg.mountText);
expectValidTranslationString(egg.notes);
expect(egg.canBuy).to.be.a('function');
expect(egg.value).to.be.a('number');
expect(egg.key).to.equal(key);
const eggTypes = [
'drops',
'quests',
];
eggTypes.forEach(eggType => {
describe(eggType, () => {
it('contains basic information about each egg', () => {
each(eggs[eggType], (egg, key) => {
expectValidTranslationString(egg.text);
expectValidTranslationString(egg.adjective);
expectValidTranslationString(egg.mountText);
expectValidTranslationString(egg.notes);
expect(egg.canBuy).to.be.a('function');
expect(egg.value).to.be.a('number');
expect(egg.key).to.equal(key);
});
});
});
});
it('does not contain unreleased eggs', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const questEggs = eggs.quests;
expect(questEggs.Giraffe).to.not.exist;
});
it('Releases eggs when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayEggs = eggs.quests;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneEggs = eggs.quests;
expect(juneEggs.Giraffe).to.exist;
expect(Object.keys(mayEggs).length).to.equal(Object.keys(juneEggs).length - 1);
});
});

View File

@@ -5,7 +5,7 @@ import {
expectValidTranslationString,
} from '../helpers/content.helper';
import { all } from '../../website/common/script/content/hatching-potions';
import hatchingPotions from '../../website/common/script/content/hatching-potions';
describe('hatchingPotions', () => {
let clock;
@@ -25,7 +25,7 @@ describe('hatchingPotions', () => {
potionTypes.forEach(potionType => {
describe(potionType, () => {
it('contains basic information about each potion', () => {
each(all, (potion, key) => {
each(hatchingPotions.all, (potion, key) => {
expectValidTranslationString(potion.text);
expectValidTranslationString(potion.notes);
expect(potion.canBuy).to.be.a('function');
@@ -35,4 +35,20 @@ describe('hatchingPotions', () => {
});
});
});
it('does not contain unreleased potions', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const premiumPotions = hatchingPotions.premium;
expect(premiumPotions.Koi).to.not.exist;
});
it('Releases potions when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayPotions = hatchingPotions.premium;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePotions = hatchingPotions.premium;
expect(junePotions.Koi).to.exist;
expect(Object.keys(mayPotions).length).to.equal(Object.keys(junePotions).length - 1);
});
});

154
test/content/index.test.js Normal file
View File

@@ -0,0 +1,154 @@
import content from '../../website/common/script/content';
describe('content index', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('Releases eggs when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const mayEggs = content.eggs;
expect(mayEggs.Chameleon).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-20'));
const juneEggs = content.eggs;
expect(juneEggs.Chameleon).to.exist;
expect(Object.keys(mayEggs).length, '').to.equal(Object.keys(juneEggs).length - 1);
});
it('Releases hatching potions when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-05-20'));
const mayHatchingPotions = content.hatchingPotions;
expect(mayHatchingPotions.Koi).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneHatchingPotions = content.hatchingPotions;
expect(juneHatchingPotions.Koi).to.exist;
expect(Object.keys(mayHatchingPotions).length, '').to.equal(Object.keys(juneHatchingPotions).length - 1);
});
it('Releases armoire gear when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneGear = content.gear.flat;
expect(juneGear.armor_armoire_corsairsCoatAndCape).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-10'));
const julyGear = content.gear.flat;
expect(julyGear.armor_armoire_corsairsCoatAndCape).to.exist;
expect(Object.keys(juneGear).length, '').to.equal(Object.keys(julyGear).length - 3);
});
it('Releases pets when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const junePets = content.petInfo;
expect(junePets['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-18'));
const julyPets = content.petInfo;
expect(julyPets['Chameleon-Base']).to.exist;
expect(Object.keys(junePets).length, '').to.equal(Object.keys(julyPets).length - 10);
});
it('Releases mounts when appropriate without needing restarting', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const juneMounts = content.mountInfo;
expect(juneMounts['Chameleon-Base']).to.not.exist;
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-07-18'));
const julyMounts = content.mountInfo;
expect(julyMounts['Chameleon-Base']).to.exist;
expect(Object.keys(juneMounts).length, '').to.equal(Object.keys(julyMounts).length - 10);
});
it('marks regular food as buyable and droppable without any events', () => {
clock = sinon.useFakeTimers(new Date('2024-06-20'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = true;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks candy as buyable and droppable during habitoween', () => {
clock = sinon.useFakeTimers(new Date('2024-10-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = true;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks cake as buyable and droppable during birthday', () => {
clock = sinon.useFakeTimers(new Date('2024-01-31'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = true;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = false;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
it('marks pie as buyable and droppable during pi day', () => {
clock = sinon.useFakeTimers(new Date('2024-03-14'));
const { food } = content;
Object.keys(food).forEach(key => {
if (key === 'Saddle') {
expect(food[key].canBuy(), `${key} canBuy`).to.be.true;
expect(food[key].canDrop, `${key} canDrop`).to.be.false;
return;
}
let expected = false;
if (key.startsWith('Cake_')) {
expected = false;
} else if (key.startsWith('Candy_')) {
expected = false;
} else if (key.startsWith('Pie_')) {
expected = true;
}
expect(food[key].canBuy(), `${key} canBuy`).to.equal(expected);
expect(food[key].canDrop, `${key} canDrop`).to.equal(expected);
});
});
});

View File

@@ -0,0 +1,82 @@
import find from 'lodash/find';
import maxBy from 'lodash/maxBy';
import {
ARMOIRE_RELEASE_DATES,
EGGS_RELEASE_DATES,
HATCHING_POTIONS_RELEASE_DATES,
} from '../../website/common/script/content/constants/releaseDates';
import armoire from '../../website/common/script/content/gear/sets/armoire';
import eggs from '../../website/common/script/content/eggs';
import hatchingPotions from '../../website/common/script/content/hatching-potions';
describe('releaseDates', () => {
let clock;
afterEach(() => {
if (clock) {
clock.restore();
}
});
describe('armoire', () => {
it('should only contain valid armoire names', () => {
const lastReleaseDate = maxBy(Object.values(ARMOIRE_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-20`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-20`));
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
expect(find(armoire.all, { set: key }), `${key} is not a valid armoire set`).to.exist;
});
});
it('should contain a valid year and month', () => {
Object.keys(ARMOIRE_RELEASE_DATES).forEach(key => {
const date = ARMOIRE_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2023);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day).to.not.exist;
});
});
});
describe('eggs', () => {
it('should only contain valid egg names', () => {
const lastReleaseDate = maxBy(Object.values(EGGS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
expect(eggs.all[key], `${key} is not a valid egg name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(EGGS_RELEASE_DATES).forEach(key => {
const date = EGGS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
describe('hatchingPotions', () => {
it('should only contain valid potion names', () => {
const lastReleaseDate = maxBy(Object.values(HATCHING_POTIONS_RELEASE_DATES), value => new Date(`${value.year}-${value.month + 1}-${value.day}`));
clock = sinon.useFakeTimers(new Date(`${lastReleaseDate.year}-${lastReleaseDate.month + 1}-${lastReleaseDate.day}`));
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
expect(hatchingPotions.all[key], `${key} is not a valid potion name`).to.exist;
});
});
it('should contain a valid year, month and date', () => {
Object.keys(HATCHING_POTIONS_RELEASE_DATES).forEach(key => {
const date = HATCHING_POTIONS_RELEASE_DATES[key];
expect(date.year, `${key} year is not a valid year`).to.be.a('number');
expect(date.year).to.be.at.least(2024);
expect(date.month, `${key} month is not a valid month`).to.be.a('number');
expect(date.month).to.be.within(1, 12);
expect(date.day, `${key} day is not a valid day`).to.be.a('number');
});
});
});
});

View File

@@ -7,7 +7,7 @@ import {
import QUEST_PETS from '../../website/common/script/content/quests/pets';
import QUEST_HATCHINGPOTIONS from '../../website/common/script/content/quests/potions';
import QUEST_BUNDLES from '../../website/common/script/content/bundles';
import { premium } from '../../website/common/script/content/hatching-potions';
import potions from '../../website/common/script/content/hatching-potions';
import SPELLS from '../../website/common/script/content/spells';
import QUEST_SEASONAL from '../../website/common/script/content/quests/seasonal';
@@ -18,12 +18,19 @@ function validateMatcher (matcher, checkedDate) {
describe('Content Schedule', () => {
let switchoverTime;
let clock;
beforeEach(() => {
switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0;
clearCachedMatchers();
});
afterEach(() => {
if (clock) {
clock.restore();
}
});
it('assembles scheduled items on january 15th', () => {
const date = new Date('2024-01-15');
const matchers = getAllScheduleMatchingGroups(date);
@@ -105,8 +112,14 @@ describe('Content Schedule', () => {
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day', () => {
const date = new Date('2024-05-07T07:00:00.000Z');
it('sets the end date if its on the release day before switchover', () => {
const date = new Date('2024-05-07T07:00:00.000+00:00');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date if its on the release day after switchover', () => {
const date = new Date('2024-05-07T09:00:00.000+00:00');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
@@ -123,12 +136,54 @@ describe('Content Schedule', () => {
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('sets the end date for a winter gala', () => {
const date = new Date('2024-12-22');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.seasonalGear.end).to.eql(moment.utc(`2025-03-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate());
});
it('uses correct date for first hours of the month', () => {
// if the date is checked before CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the previous month
const date = new Date('2024-05-01T02:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.true;
expect(matchers.timeTravelers.match('202404'), '202404').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.false;
});
it('uses correct date after switchover time', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
const date = new Date('2024-05-01T09:00:00.000Z');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
});
it('uses UTC timezone', () => {
// if the date is checked after CONTENT_SWITCHOVER_TIME_OFFSET,
// it should be considered the current
clock = sinon.useFakeTimers(new Date('2024-05-01T05:00:00.000-04:00'));
const matchers = getAllScheduleMatchingGroups();
expect(matchers.petQuests.items).to.contain('snake');
expect(matchers.petQuests.items).to.not.contain('horse');
expect(matchers.timeTravelers.match('202304'), '202304').to.be.false;
expect(matchers.timeTravelers.match('202305'), '202305').to.be.true;
expect(matchers.timeTravelers.match('202405'), '202405').to.be.false;
});
it('contains content for repeating events', () => {
const date = new Date('2024-04-15');
const matchers = getAllScheduleMatchingGroups(date);
expect(matchers.premiumHatchingPotions).to.exist;
expect(matchers.premiumHatchingPotions.items.length).to.equal(4);
expect(matchers.premiumHatchingPotions.items.indexOf('Garden')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.length).to.equal(5);
expect(matchers.premiumHatchingPotions.items.indexOf('Veggie')).to.not.equal(-1);
expect(matchers.premiumHatchingPotions.items.indexOf('Porcelain')).to.not.equal(-1);
});
@@ -167,7 +222,7 @@ describe('Content Schedule', () => {
});
it('premium hatching potions', () => {
const potionKeys = Object.keys(premium);
const potionKeys = Object.keys(potions.premium);
Object.keys(MONTHLY_SCHEDULE).forEach(key => {
const monthlyPotions = MONTHLY_SCHEDULE[key][21].find(item => item.type === 'premiumHatchingPotions');
for (const potion of monthlyPotions.items) {
@@ -245,27 +300,33 @@ describe('Content Schedule', () => {
it('allows sets matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202307')).to.be.true;
expect(matcher.match('202207')).to.be.true;
expect(matcher.match('202307'), '202307').to.be.true;
expect(matcher.match('202207'), '202207').to.be.true;
});
it('disallows sets not matching the month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202306')).to.be.false;
expect(matcher.match('202402')).to.be.false;
expect(matcher.match('202306'), '202306').to.be.false;
expect(matcher.match('202402'), '202402').to.be.false;
});
it('disallows sets from current month', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202407')).to.be.false;
expect(matcher.match('202407'), '202407').to.be.false;
});
it('disallows sets from the future', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).backgrounds;
expect(matcher.match('202507')).to.be.false;
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202507'), '202507').to.be.false;
});
it('matches sets released in the earlier half of the year', () => {
const date = new Date('2024-07-08');
const matcher = getAllScheduleMatchingGroups(date).timeTravelers;
expect(matcher.match('202401'), '202401').to.be.true;
});
});
});

View File

@@ -6,12 +6,21 @@ import {
} from '../helpers/content.helper';
import t from '../../website/common/script/content/translation';
import * as stable from '../../website/common/script/content/stable';
import * as eggs from '../../website/common/script/content/eggs';
import * as potions from '../../website/common/script/content/hatching-potions';
import stable from '../../website/common/script/content/stable';
import eggs from '../../website/common/script/content/eggs';
import potions from '../../website/common/script/content/hatching-potions';
describe('stable', () => {
describe('dropPets', () => {
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers(new Date('2020-05-20'));
});
afterEach(() => {
clock.restore();
});
it('contains a pet for each drop potion * each drop egg', () => {
const numberOfDropPotions = Object.keys(potions.drops).length;
const numberOfDropEggs = Object.keys(eggs.drops).length;

View File

@@ -45,7 +45,7 @@ describe('time-travelers store', () => {
describe('on may 1st', () => {
beforeEach(() => {
date = new Date('2024-05-01');
date = new Date('2024-05-01T09:00:00.000Z');
});
it('returns the correct gear', () => {
const items = timeTravelers.timeTravelerStore(user, date);

View File

@@ -27,8 +27,15 @@
</head>
<body>
<div id="loading-screen">
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.62 28.86c-.32-.706-1.057-1.048-1.538-.706-.48.341-1.147.393-1.78.24-.633-.153-.753-1.604-.616-3.278.136-1.673.363-2.318.506-2.925.162-.61.877-.562.962-.084.086.479.582.479 1.307-.391.724-.87.617-3.409-.218-5.474-.836-2.065.326-1.865.664-1.66.337.205.544-.102.462-1.28-.082-1.178-1.166-2.098-2.039-2.663-.873-.564-1.936-1.186-1.911-2.697.025-1.511 2.08-1.464 2.358-1.439.279.025.815-.093.506-1.663-.31-1.57-1.43-1.869-2.133-1.826-.703.042-1.177.428-2.17.053-.995-.376-1.655-.23-2.58-.023-.926.206-2.138.776-3.646 1.183-.795.219-1.064.274-1.93.288-.532.008-.755.653-.043 1.444.563.643 1.839.814 2.606.707.494-.07.608.258.563.74a8.013 8.013 0 0 0-.01 1.795c.08.6.18 1.62-.103 2.286-.14.326-.545.677-.98.653-.565-.034-1.022-.7-1.414-1.49-.825-1.662-1.793-2.014-5.404-3.535-3.248-1.367-5.007-3.5-6.096-4.874-.969-1.217-1.939-.756-1.85.342.07.852.592 3.604 1.912 5.257 1.623 2.525 4.128 3.67 7.013 3.895.755.06 1.226.208 1.29.553.095.735-.622 1.244-1.959 1.09-1.336-.157-1.907.087-1.641.848.85 1.79 2.809 1.869 3.623 1.942.275.05 1.246 0 1.764.143.605.166.735 1.005-.14 1.459-1.558.76-2.237 1.391-3.025 2.83-.595 1.13-1.108 3.022-.574 5.745.513 2.648-3.337 2.733-5 2.357-.716-.151-1.47-1.512.287-2.65 1.421-.922 1.708-1.49 1.645-2.657-.074-1.36-.824-1.458-.822-2.64v-2.82a.435.435 0 0 0-.435-.435H7.698a.435.435 0 0 1-.435-.434v-1.7a.435.435 0 0 0-.435-.435H5.501a.435.435 0 0 1-.435-.435v-1.524a.435.435 0 0 0-.435-.435H3.015a.435.435 0 0 1-.435-.435v-1.603a.435.435 0 0 0-.435-.434H.435a.435.435 0 0 0-.435.434v1.705c0 .24.195.435.435.435h1.62c.24 0 .435.195.435.435v6.076c0 .241.195.435.435.435h1.71c.241 0 .436.196.436.435v1.988c0 .24.195.434.435.434h2.402c.734-.052.862.934.854 1.286-.016.803-.923 1.06-1.352 1.395-1.145.884-2.031 1.783-1.513 3.512l.013.036c.945 2.007 3.542 1.8 5.183 1.8h10.326c.584 0 1.184.135 1.046-.545-.136-.68-.425-1.61-1.265-1.61-.84 0-.703.467-1.524.228-.821-.238-.822-1.348.411-3.279 1.276-1.649 3.46-1.524 4.781-.358 1.32 1.166.93 3.191.653 4.354-.158.82.218 1.224.669 1.213h5.242c.806-.014.647-.556.185-1.614h.003z" fill="#fff"/>
<svg width="80" height="80" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
d="M79.05 72.15c-.8-1.766-2.643-2.62-3.845-1.766-1.201.855-2.867.985-4.448.602-1.584-.385-1.885-4.01-1.543-8.195.342-4.184.909-5.795 1.267-7.314.404-1.524 2.191-1.404 2.405-.209.215 1.196 1.454 1.196 3.266-.979 1.811-2.175 1.543-8.52-.546-13.684-2.088-5.163.817-4.661 1.66-4.149.844.513 1.362-.255 1.156-3.2-.204-2.945-2.916-5.247-5.096-6.657-2.184-1.41-4.842-2.967-4.78-6.745.063-3.777 5.2-3.658 5.897-3.596.697.063 2.037-.233 1.264-4.157-.773-3.924-3.575-4.673-5.332-4.567-1.758.106-2.943 1.071-5.427.133-2.484-.938-4.136-.572-6.45-.057-2.313.515-5.343 1.94-9.112 2.959-1.989.545-2.661.683-4.828.718-1.33.02-1.885 1.633-.106 3.61 1.408 1.608 4.597 2.036 6.515 1.768 1.236-.174 1.521.645 1.407 1.85a20.023 20.023 0 0 0-.024 4.488c.198 1.5.45 4.051-.258 5.713-.35.817-1.361 1.693-2.449 1.633-1.413-.084-2.555-1.75-3.537-3.726-2.06-4.152-4.48-5.033-13.509-8.835-8.12-3.417-12.516-8.749-15.24-12.185-2.421-3.042-4.846-1.89-4.626.855.179 2.128 1.48 9.008 4.781 13.141 4.058 6.314 10.32 9.177 17.534 9.739 1.885.149 3.065.52 3.225 1.383.236 1.835-1.557 3.11-4.898 2.722-3.341-.39-4.768.22-4.103 2.121 2.123 4.477 7.021 4.672 9.058 4.857.686.122 3.114 0 4.41.355 1.51.418 1.836 2.514-.353 3.648-3.892 1.903-5.59 3.479-7.561 7.075-1.486 2.826-2.77 7.555-1.435 14.365 1.283 6.62-8.342 6.83-12.497 5.89-1.793-.377-3.675-3.778.716-6.625 3.553-2.305 4.269-3.724 4.111-6.642-.184-3.4-2.058-3.644-2.053-6.598v-7.05c0-.602-.488-1.088-1.087-1.088h-3.334a1.087 1.087 0 0 1-1.087-1.087v-4.25c0-.602-.488-1.087-1.088-1.087h-3.317a1.087 1.087 0 0 1-1.087-1.088v-3.81c0-.602-.489-1.087-1.088-1.087h-4.04a1.087 1.087 0 0 1-1.089-1.088V26.25c0-.602-.488-1.088-1.087-1.088H1.088C.485 25.161 0 25.65 0 26.25v4.26c0 .602.488 1.087 1.088 1.087h4.049c.602 0 1.087.489 1.087 1.088v15.192c0 .602.489 1.087 1.088 1.087h4.277c.602 0 1.088.489 1.088 1.088v4.968c0 .602.488 1.087 1.087 1.087h6.005c1.836-.13 2.156 2.335 2.137 3.214-.04 2.007-2.308 2.652-3.382 3.487-2.861 2.21-5.077 4.459-3.78 8.781l.032.09c2.362 5.017 8.855 4.499 12.956 4.499h25.817c1.459 0 2.959.339 2.614-1.362-.342-1.7-1.063-4.024-3.162-4.024-2.1 0-1.758 1.166-3.81.57-2.054-.597-2.057-3.371 1.027-8.198 3.19-4.122 8.652-3.81 11.952-.895 3.301 2.915 2.325 7.978 1.633 10.885-.396 2.048.545 3.06 1.67 3.032H78.58c2.015-.035 1.62-1.391.464-4.035h.008z"
fill="#fff"
>
</path>
</svg>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -27,73 +27,15 @@
</div>
</div>
</div>
<div
id="app"
:class="{
'casting-spell': castingSpell,
}"
>
<!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" />
<payments-success-modal />
<sub-cancel-modal-confirm v-if="isUserLoaded" />
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
<birthday-banner />
<notifications-display />
<app-menu />
<div
class="container-fluid"
:class="{'no-margin': noMargin}"
>
<app-header />
<buyModal
:item="selectedItemToBuy || {}"
:with-pin="true"
:generic-purchase="genericPurchase(selectedItemToBuy)"
@buyPressed="customPurchase($event)"
/>
<selectMembersModal
:item="selectedSpellToBuy || {}"
:group="user.party"
@memberSelected="memberSelected($event)"
/>
<div :class="{sticky: user.preferences.stickyHeader}">
<router-view />
</div>
</div>
<app-footer v-if="!hideFooter" />
<audio
id="sound"
ref="sound"
autoplay="autoplay"
></audio>
</template>
</template>
</div>
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<user-main v-else />
</div>
</template>
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
#app {
display: flex;
flex-direction: column;
overflow-x: hidden;
}
#loading-screen-inapp {
#melior {
color: $white;
@@ -163,68 +105,20 @@
<script>
import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo';
import BirthdayBanner from './components/header/banners/birthdayBanner';
import AppFooter from './components/appFooter';
import notificationsDisplay from './components/notifications';
import snackbars from './components/snackbars/notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from './components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
import { setup as setupPayments } from '@/libs/payments';
import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import spellsMixin from '@/mixins/spells';
import {
CONSTANTS,
getLocalSetting,
removeLocalSetting,
} from '@/libs/userlocalManager';
const bugReportModal = () => import(/* webpackChunkName: "bug-report-modal" */'@/components/bugReportModal');
const bugReportSuccessModal = () => import(/* webpackChunkName: "bug-report-success-modal" */'@/components/bugReportSuccessModal');
import { mapState } from '@/libs/store';
import userMain from '@/pages/user-main';
import snackbars from '@/components/snackbars/notifications';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
name: 'App',
components: {
AppMenu,
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,
BirthdayBanner,
notificationsDisplay,
snackbars,
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
paymentsSuccessModal,
subCancelModalConfirm,
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
userMain,
},
mixins: [notifications, spellsMixin],
data () {
return {
selectedItemToBuy: null,
@@ -238,71 +132,25 @@ export default {
};
},
computed: {
...mapState(['isUserLoggedIn', 'browserTimezoneUtcOffset', 'isUserLoaded', 'notificationsRemoved']),
...mapState(['isUserLoggedIn', 'isUserLoaded', 'notificationsRemoved']),
...mapState({ user: 'user.data' }),
isStaticPage () {
return this.$route.meta.requiresLogin === false;
},
castingSpell () {
return this.$store.state.spellOptions.castingSpell;
},
noMargin () {
return ['privateMessages'].includes(this.$route.name);
},
hideFooter () {
return ['privateMessages'].includes(this.$route.name);
},
},
created () {
this.$root.$on('playSound', sound => {
const theme = this.user.preferences.sound;
if (!theme || theme === 'off') {
return;
}
const file = `/static/audio/${theme}/${sound}`;
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
});
// @TODO: I'm not sure these should be at the app level.
// Can we move these back into shop/inventory or maybe they need a lateral move?
this.$root.$on('buyModal::showItem', item => {
this.selectedItemToBuy = item;
this.$root.$emit('bv::show::modal', 'buy-modal');
});
this.$root.$on('bv::modal::hidden', event => {
if (event.componentId === 'buy-modal') {
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
this.$store.watch(state => state.isUserLoaded, () => {
if (this.isUserLoaded) {
this.hideLoadingScreen();
}
});
this.$root.$on('selectMembersModal::showItem', item => {
this.selectedSpellToBuy = item;
this.$root.$emit('bv::show::modal', 'select-member-modal');
});
// @TODO split up this file, it's too big
loadProgressBar({
showSpinner: false,
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
axios.interceptors.response.use(response => { // Set up Response interceptors
@@ -414,79 +262,20 @@ export default {
return Promise.reject(error);
});
// Setup listener for title
this.$store.watch(state => state.title, title => {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
Analytics.setUser();
Analytics.updateUser();
return axios.get(
'/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
this.hideLoadingScreen();
// Adjust the timezone offset
const browserTimezoneOffset = -this.browserTimezoneUtcOffset;
if (this.user.preferences.timezoneOffset !== browserTimezoneOffset) {
this.$store.dispatch('user:set', {
'preferences.timezoneOffset': browserTimezoneOffset,
});
}
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState) {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
this.$root.$emit('habitica:payment-success', appState);
}
}
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
});
}).catch(err => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
});
} else {
this.hideLoadingScreen();
}
},
beforeDestroy () {
this.$root.$off('playSound');
this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem');
},
mounted () {
// Remove the index.html loading screen and now show the inapp loading
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) document.body.removeChild(loadingScreen);
if (this.isStaticPage || !this.isUserLoggedIn) {
this.hideLoadingScreen();
}
},
methods: {
hideLoadingScreen () {
this.loading = false;
},
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
@@ -507,57 +296,10 @@ export default {
this.$store.dispatch('auth:logout', { redirectToLogin: true });
return true;
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
genericPurchase (item) {
if (!item) return false;
if (['card', 'debuffPotion'].includes(item.purchaseType)) return false;
return true;
},
customPurchase (item) {
if (item.purchaseType === 'card') {
this.selectedSpellToBuy = item;
// hide the dialog
this.$root.$emit('bv::hide::modal', 'buy-modal');
// remove the dialog from our modal-stack,
// the default hidden event is delayed
this.$root.$emit('bv::modal::hidden', {
target: {
id: 'buy-modal',
},
});
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
if (item.purchaseType === 'debuffPotion') {
this.castStart(item, this.user);
}
},
async memberSelected (member) {
await this.castStart(this.selectedSpellToBuy, member);
this.selectedSpellToBuy = null;
if (this.user.party._id) {
this.$store.dispatch('party:getMembers', { forceLoad: true });
}
this.$root.$emit('bv::hide::modal', 'select-member-modal');
},
hideLoadingScreen () {
this.loading = false;
},
},
};
</script>
<style src="intro.js/minified/introjs.min.css"></style>
<style src="axios-progress-bar/dist/nprogress.css"></style>
<style src="@/assets/scss/index.scss" lang="scss"></style>
<style src="@/assets/scss/sprites.scss" lang="scss"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>

File diff suppressed because it is too large Load Diff

View File

@@ -174,6 +174,30 @@
}
}
.btn-warning {
background: $orange-10;
color: $white !important;
&:hover:not(:disabled):not(.disabled) {
background: $orange-100;
color: $white;
}
&:focus {
background: $orange-10;
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active:focus, &:not(:disabled):not(.disabled).active:focus {
box-shadow: none;
border-color: $purple-400;
}
&:not(:disabled):not(.disabled):active, &:not(:disabled):not(.disabled).active {
background: $orange-10;
}
}
.btn-success {
background: $green-50;
border: 1px solid transparent;

View File

@@ -23,16 +23,14 @@
{{ $t('foundNewItems') }}
</h2>
<div class="d-flex justify-content-center">
<div
<Sprite
class="item-box ml-auto mr-3"
:class="eggClass"
>
</div>
<div
:image-name="eggClass"
/>
<Sprite
class="item-box mr-auto"
:class="potionClass"
>
</div>
:image-name="potionClass"
/>
</div>
<p
v-once
@@ -103,8 +101,12 @@
<script>
import closeIcon from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
data () {
return {
icons: Object.freeze({

View File

@@ -19,10 +19,10 @@
</div>
<div class="inner-content">
<div class="achievement-background d-flex align-items-center">
<div
<Sprite
class="icon"
:class="achievementClass"
></div>
:image-name="achievementClass"
/>
</div>
<h4
class="title"
@@ -99,8 +99,12 @@
import achievements from '@/../../common/script/content/achievements';
import { mapState } from '@/libs/store';
import svgClose from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
props: ['data'],
data () {
return {

View File

@@ -1,30 +1,41 @@
<template>
<div class="row standard-page">
<div class="well col-12">
<div class="row standard-page col-12 d-flex justify-content-center">
<div class="admin-panel-content">
<h1>Admin Panel</h1>
<div>
<form
class="form-inline"
@submit.prevent="loadHero(userIdentifier)"
>
<form
class="form-inline"
@submit.prevent="searchUsers(userIdentifier)"
>
<div class="input-group col pl-0 pr-0">
<input
v-model="userIdentifier"
class="form-control uidField"
class="form-control"
type="text"
:placeholder="'User ID or Username; blank for your account'"
:placeholder="'UserID, username, email, or leave blank for your account'"
>
<input
type="submit"
value="Load User"
class="btn btn-secondary"
>
</form>
</div>
<div class="input-group-append">
<button
class="btn btn-primary"
type="button"
@click="loadUser(userIdentifier)"
>
Load User
</button>
<button
class="btn btn-secondary"
type="button"
@click="searchUsers(userIdentifier)"
>
Search
</button>
</div>
</div>
</form>
<div>
<router-view @changeUserIdentifier="changeUserIdentifier" />
</div>
<router-view
class="mt-3"
@changeUserIdentifier="changeUserIdentifier"
/>
</div>
</div>
</template>
@@ -33,6 +44,15 @@
.uidField {
min-width: 45ch;
}
.input-group-append {
width:auto;
}
.admin-panel-content {
flex: 0 0 800px;
max-width: 800px;
}
</style>
<script>
@@ -62,7 +82,24 @@ export default {
// (useful if we want to re-fetch the user after making changes).
this.userIdentifier = newId;
},
async loadHero (userIdentifier) {
async searchUsers (userIdentifier) {
if (!userIdentifier || userIdentifier === '') {
this.loadUser();
return;
}
this.$router.push({
name: 'adminPanelSearch',
params: { userIdentifier },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
},
async loadUser (userIdentifier) {
const id = userIdentifier || this.user._id;
this.$router.push({

View File

@@ -0,0 +1,159 @@
<template>
<div>
<div
v-if="noUsersFound"
class="alert alert-warning"
role="alert"
>
Could not find any matching users.
</div>
<loading-spinner
v-if="isSearching"
class="mx-auto mb-2"
dark-color="true"
/>
<div
v-if="users.length > 0"
class="list-group"
>
<a
v-for="user in users"
:key="user._id"
href="#"
class="list-group-item list-group-item-action"
@click="loadUser(user._id)"
>
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">{{ user.profile.name }}</h5>
<small>{{ user._id }}</small>
</div>
<p
class="mb-1"
:class="{'highlighted-value': matchValueToIdentifier(user.auth.local.username)}"
>
@{{ user.auth.local.username }}</p>
<p class="mb-0">
<span
v-for="email in userEmails(user)"
:key="email"
:class="{'highlighted-value': matchValueToIdentifier(email)}"
>
{{ email }}
</span>
</p>
</a>
</div>
</div>
</template>
<style lang="scss" scoped>
.highlighted-value {
font-weight: bold;
}
</style>
<script>
import VueRouter from 'vue-router';
import { mapState } from '@/libs/store';
import LoadingSpinner from '../ui/loadingSpinner';
const { isNavigationFailure, NavigationFailureType } = VueRouter;
export default {
components: {
LoadingSpinner,
},
data () {
return {
userIdentifier: '',
users: [],
noUsersFound: false,
isSearching: false,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
watch: {
userIdentifier () {
this.isSearching = true;
this.$store.dispatch('adminPanel:searchUsers', { userIdentifier: this.userIdentifier }).then(users => {
this.isSearching = false;
if (users.length === 1) {
this.loadUser(users[0]._id);
} else {
const matchIndex = users.findIndex(user => this.isExactMatch(user));
if (matchIndex !== -1) {
users.splice(0, 0, users.splice(matchIndex, 1)[0]);
}
this.users = users;
this.noUsersFound = users.length === 0;
}
});
this.$emit('changeUserIdentifier', this.userIdentifier); // change user identifier in Admin Panel's form
},
},
mounted () {
this.userIdentifier = this.$route.params.userIdentifier;
},
methods: {
matchValueToIdentifier (value) {
return value.toLowerCase().includes(this.userIdentifier.toLowerCase());
},
userEmails (user) {
const allEmails = [];
if (user.auth.local.email) allEmails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.emails) {
const emails = user.auth.google.emails;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.apple && user.auth.apple.emails) {
const emails = user.auth.apple.emails;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.facebook && user.auth.facebook.emails) {
const emails = user.auth.facebook.emails;
allEmails.push(...this.findSocialEmails(emails));
}
return allEmails;
},
findSocialEmails (emails) {
if (typeof emails === 'string') return [emails];
if (Array.isArray(emails)) return emails.map(email => email.value);
if (typeof emails === 'object') return [emails.value];
return [];
},
async loadUser (userIdentifier) {
const id = userIdentifier || this.user._id;
this.$router.push({
name: 'adminPanelUser',
params: { userIdentifier: id },
}).catch(failure => {
if (isNavigationFailure(failure, NavigationFailureType.duplicated)) {
// the admin has requested that the same user be displayed again so reload the page
// (e.g., if they changed their mind about changes they were making)
this.$router.go();
}
});
},
isExactMatch (user) {
return user._id === this.userIdentifier
|| user.auth.local.username === this.userIdentifier
|| (user.auth.google && user.auth.google.emails && user.auth.google.emails.findIndex(
email => email.value === this.userIdentifier,
) !== -1)
|| (user.auth.apple && user.auth.apple.emails && user.auth.apple.emails.findIndex(
email => email.value === this.userIdentifier,
) !== -1)
|| (user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails.findIndex(
email => email.value === this.userIdentifier,
) !== -1);
},
},
};
</script>

View File

@@ -1,13 +1,18 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Achievements
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Achievements
</h3>
<div v-if="expand">
<ul>
<li
v-for="item in achievements"
@@ -251,11 +256,14 @@ export default {
},
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
this.hero.achievementPath = item.path;
this.hero.achievementVal = item.value;
await this.saveHero({ hero: this.hero, msg: item.path });
await this.saveHero({
hero: {
_id: this.hero._id,
achievementPath: item.path,
achievementVal: item.value,
},
msg: item.path,
});
item.modified = false;
},
enableValueChange (item) {

View File

@@ -1,13 +1,18 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Current Avatar Appearance, Drop Count Today
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Current Avatar Appearance, Drop Count Today
</h3>
<div v-if="expand">
<div>Drops Today: {{ items.lastDrop.count }}</div>
<div>Most Recent Drop: {{ items.lastDrop.date | formatDate }}</div>
<div>Use Costume: {{ preferences.costume ? 'on' : 'off' }}</div>

View File

@@ -1,160 +1,134 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Contributor Details
</h3>
<div v-if="expand">
<form @submit.prevent="saveHero({hero, msg: 'Contributor details', clearData: true})">
<div>
<label>Permissions</label>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.fullAccess"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Full Admin Access (Allows access to everything. EVERYTHING)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.userSupport"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
User Support (Access this form, access purchase history)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.news"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
News poster (Bailey CMS)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.moderator"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Community Moderator (ban and mute users, access chat flags, manage social spaces)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.challengeAdmin"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Challenge Admin (can create official habitica challenges and admin all challenges)
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.permissions.coupons"
:disabled="!hasPermission(user, 'fullAccess')"
type="checkbox"
>
Coupon Creator (can manage coupon codes)
</label>
</div>
</div>
<div class="form-group">
<label>Title</label>
<input
v-model="hero.contributor.text"
class="form-control textField"
type="text"
>
<small>
Common titles:
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
<br>
Rare titles:
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
Statistician, Tinker, Transcriber, Troubadour.
</small>
</div>
<div class="form-group form-inline">
<label>Tier</label>
<input
v-model="hero.contributor.level"
class="form-control levelField"
type="number"
>
<small>
1-7 for normal contributors, 8 for moderators, 9 for staff.
This determines which items, pets, mounts are available, and name-tag coloring.
Tiers 8 and 9 are automatically given admin status.
</small>
</div>
<div
v-if="hero.secret.text"
class="form-group"
<form @submit.prevent="saveHero({ hero: {
_id: hero._id,
contributor: hero.contributor,
secret: hero.secret,
permissions: hero.permissions,
}, msg: 'Contributor details', clearData: true })">
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{ 'open': expand }"
@click="expand = !expand"
>
<label>Moderation Notes</label>
Contributor Details
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div class="mb-4">
<h3 class="mt-0">
Permissions
</h3>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
v-for="permission in permissionList"
:key="permission.key"
class="col-sm-9 offset-sm-3"
>
<div class="custom-control custom-checkbox">
<input
v-model="hero.permissions[permission.key]"
:disabled="!hasPermission(user, permission.key)"
class="custom-control-input"
type="checkbox"
>
<label class="custom-control-label">
{{ permission.name }}<br>
<small class="text-secondary">{{ permission.description }}</small>
</label>
</div>
</div>
</div>
<div class="form-group">
<label>Contributions</label>
<textarea
v-model="hero.contributor.contributions"
class="form-control"
cols="5"
rows="5"
></textarea>
<div
v-markdown="hero.contributor.contributions"
class="markdownPreview"
></div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Title</label>
<div class="col-sm-9">
<input
v-model="hero.contributor.text"
class="form-control textField"
type="text"
>
<small>
Common titles:
<strong>Ambassador, Artisan, Bard, Blacksmith, Challenger, Comrade, Fletcher,
Linguist, Linguistic Scribe, Scribe, Socialite, Storyteller</strong>.
<br>
Rare titles:
Advisor, Chamberlain, Designer, Mathematician, Shirtster, Spokesperson,
Statistician, Tinker, Transcriber, Troubadour.
</small>
</div>
</div>
<div class="form-group">
<label>Edit Moderation Notes</label>
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="3"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Tier</label>
<div class="col-sm-9">
<input
v-model="hero.contributor.level"
class="form-control levelField"
type="number"
>
<small>
1-7 for normal contributors, 8 for moderators, 9 for staff.
This determines which items, pets, mounts are available, and name-tag coloring.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Contributions</label>
<div class="col-sm-9">
<textarea
v-model="hero.contributor.contributions"
class="form-control"
cols="5"
rows="5"
>
</textarea>
<div
v-markdown="hero.contributor.contributions"
class="markdownPreview"
></div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Moderation Notes</label>
<div class="col-sm-9">
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="3"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save and Clear Data"
class="btn btn-primary"
value="Save"
class="btn btn-primary mt-1"
>
</form>
</div>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
.levelField {
min-width: 10ch;
}
.textField {
min-width: 50ch;
}
.levelField {
min-width: 10ch;
}
.textField {
min-width: 50ch;
}
</style>
<script>
@@ -164,6 +138,39 @@ import saveHero from '../mixins/saveHero';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../../mixins/userState';
const permissionList = [
{
key: 'fullAccess',
name: 'Full Admin Access',
description: 'Allows access to everything. EVERYTHING',
},
{
key: 'userSupport',
name: 'User Support',
description: 'Access this form, access purchase history',
},
{
key: 'news',
name: 'News Poster',
description: 'Bailey CMS',
},
{
key: 'moderator',
name: 'Community Moderator',
description: 'Ban and mute users, access chat flags, manage social spaces',
},
{
key: 'challengeAdmin',
name: 'Challenge Admin',
description: 'Can create official habitica challenges and admin all challenges',
},
{
key: 'coupons',
name: 'Coupon Creator',
description: 'Can manage coupon codes',
},
];
function resetData (self) {
self.expand = self.hero.contributor.level;
}
@@ -192,6 +199,7 @@ export default {
data () {
return {
expand: false,
permissionList,
};
},
watch: {

View File

@@ -1,145 +1,197 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Timestamps, Time Zone, Authentication, Email Address
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
<div v-if="expand">
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
auth: hero.auth,
preferences: hero.preferences,
}, msg: 'Authentication' })"
>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Timestamps, Time Zone, Authentication, Email Address
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
See error(s) below.
</p>
<div>
Account created:
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
</div>
<div v-if="hero.flags.thirdPartyTools">
User has employed <strong>third party tools</strong>. Last known usage:
<strong>{{ hero.flags.thirdPartyTools | formatDate }}</strong>
</div>
<div v-if="cronError">
"lastCron" value:
<strong>{{ hero.lastCron | formatDate }}</strong>
<br>
<span class="errorMessage">
ERROR: cron probably crashed before finishing
("auth.timestamps.loggedin" and "lastCron" dates are different).
</span>
</div>
<div class="form-inline">
<div>
Most recent cron:
<strong>{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
("auth.timestamps.loggedin")
</div>
<button
class="btn btn-primary ml-2"
@click="resetCron()"
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
>
Reset Cron to Yesterday
</button>
</div>
<div class="subsection-start">
Time zone:
<strong>{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
</div>
<div>
Custom Day Start time (CDS):
<strong>{{ hero.preferences.dayStart }}</strong>
</div>
<div v-if="timezoneDiffError || timezoneMissingError">
Time zone at previous cron:
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
See error(s) below.
</p>
<div class="errorMessage">
<div v-if="timezoneDiffError">
ERROR: the player's current time zone is different than their time zone when
their previous cron ran. This can be because:
<ul>
<li>daylight savings started or stopped <sup>*</sup></li>
<li>the player changed zones due to travel <sup>*</sup></li>
<li>the player has devices set to different zones <sup>**</sup></li>
<li>the player uses a VPN with varying zones <sup>**</sup></li>
<li>something similarly unpleasant is happening. <sup>**</sup></li>
</ul>
<p>
<em>* The problem should fix itself in about a day.</em><br>
<em>** One of these causes is probably happening if the time zones stay
different for more than a day.</em>
</p>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Account created:</label>
<strong class="col-sm-9 col-form-label">
{{ hero.auth.timestamps.created | formatDate }}</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Used third party tools:</label>
<div v-if="timezoneMissingError">
ERROR: One of the player's time zones is missing.
This is expected and okay if it's the "Time zone at previous cron"
AND if it's their first day in Habitica.
Otherwise an error has occurred.
<div class="col-sm-9 col-form-label">
<strong v-if="hero.flags.thirdPartyTools">
Yes - {{ hero.flags.thirdPartyTools | formatDate }}</strong>
<strong v-else>No</strong>
</div>
</div>
</div>
<div class="subsection-start form-inline">
API Token: &nbsp;
<form @submit.prevent="changeApiToken()">
<input
type="submit"
value="Change API Token"
class="btn btn-primary"
>
</form>
<div
v-if="tokenModified"
class="form-inline"
v-if="cronError"
class="form-group row"
>
<strong>API Token has been changed. Tell the player something like this:</strong>
<label class="col-sm-3 col-form-label">lastCron value:</label>
<strong>{{ hero.lastCron | formatDate }}</strong>
<br>
I've given you a new API Token.
You'll need to log out of the website and mobile app then log back in
otherwise they won't work correctly.
If you have trouble logging out, for the website go to
https://habitica.com/static/clear-browser-data and click the red button there,
and for the Android app, clear its data.
For the iOS app, if you can't log out you might need to uninstall it,
reboot your phone, then reinstall it.
<span class="errorMessage">
ERROR: cron probably crashed before finishing
("auth.timestamps.loggedin" and "lastCron" dates are different).
</span>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Most recent cron:</label>
<div class="col-sm-9 col-form-label">
<strong>
{{ hero.auth.timestamps.loggedin | formatDate }}</strong>
<a
class="btn btn-warning btn-sm ml-4"
@click="resetCron()"
>
Reset Cron to Yesterday
</a>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Time zone:</label>
<strong class="col-sm-9 col-form-label">
{{ hero.preferences.timezoneOffset | formatTimeZone }}</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Custom Day Start time (CDS)</label>
<div class="col-sm-9">
<input
v-model="hero.preferences.dayStart"
class="form-control levelField"
type="number"
>
</div>
</div>
<div v-if="timezoneDiffError || timezoneMissingError">
Time zone at previous cron:
<strong>{{ hero.preferences.timezoneOffsetAtLastCron | formatTimeZone }}</strong>
<div class="errorMessage">
<div v-if="timezoneDiffError">
ERROR: the player's current time zone is different than their time zone when
their previous cron ran. This can be because:
<ul>
<li>daylight savings started or stopped <sup>*</sup></li>
<li>the player changed zones due to travel <sup>*</sup></li>
<li>the player has devices set to different zones <sup>**</sup></li>
<li>the player uses a VPN with varying zones <sup>**</sup></li>
<li>something similarly unpleasant is happening. <sup>**</sup></li>
</ul>
<p>
<em>* The problem should fix itself in about a day.</em><br>
<em>** One of these causes is probably happening if the time zones stay
different for more than a day.</em>
</p>
</div>
<div v-if="timezoneMissingError">
ERROR: One of the player's time zones is missing.
This is expected and okay if it's the "Time zone at previous cron"
AND if it's their first day in Habitica.
Otherwise an error has occurred.
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">API Token</label>
<div class="col-sm-9">
<a
href="#"
value="Change API Token"
class="btn btn-danger"
@click="changeApiToken()"
>
Change API Token
</a>
<div
v-if="tokenModified"
>
<strong>API Token has been changed. Tell the player something like this:</strong>
<br>
I've given you a new API Token.
You'll need to log out of the website and mobile app then log back in
otherwise they won't work correctly.
If you have trouble logging out, for the website go to
https://habitica.com/static/clear-browser-data and click the red button there,
and for the Android app, clear its data.
For the iOS app, if you can't log out you might need to uninstall it,
reboot your phone, then reinstall it.
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Local Authentication E-Mail</label>
<div class="col-sm-9">
<input
v-model="hero.auth.local.email"
class="form-control"
type="text"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Google authentication</label>
<div class="col-sm-9">
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
<span v-else><strong>None</strong></span>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Facebook authentication</label>
<div class="col-sm-9">
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
<span v-else><strong>None</strong></span>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Apple ID authentication</label>
<div class="col-sm-9">
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
<span v-else><strong>None</strong></span>
</div>
</div>
<div class="subsection-start">
Full "auth" object for checking above is correct:
<pre>{{ hero.auth }}</pre>
</div>
</div>
<div class="subsection-start">
Local authentication:
<span v-if="hero.auth.local.email">Yes, &nbsp;
<strong>{{ hero.auth.local.email }}</strong></span>
<span v-else><strong>None</strong></span>
</div>
<div>
Google authentication:
<pre v-if="authMethodExists('google')">{{ hero.auth.google }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div>
Facebook authentication:
<pre v-if="authMethodExists('facebook')">{{ hero.auth.facebook }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div>
Apple ID authentication:
<pre v-if="authMethodExists('apple')">{{ hero.auth.apple }}</pre>
<span v-else><strong>None</strong></span>
</div>
<div class="subsection-start">
Full "auth" object for checking above is correct:
<pre>{{ hero.auth }}</pre>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
</div>
</div>
</div>
</form>
</template>
<script>
@@ -226,13 +278,24 @@ export default {
return false;
},
async changeApiToken () {
this.hero.changeApiToken = true;
await this.saveHero({ hero: this.hero, msg: 'API Token' });
await this.saveHero({
hero: {
_id: this.hero._id,
changeApiToken: true,
},
msg: 'API Token',
});
this.tokenModified = true;
},
resetCron () {
this.hero.resetCron = true;
this.saveHero({ hero: this.hero, msg: 'Last Cron', clearData: true });
this.saveHero({
hero: {
_id: this.hero._id,
resetCron: true,
},
msg: 'Last Cron',
clearData: true,
});
},
},
};

View File

@@ -1,13 +1,18 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Customizations
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Customizations
</h3>
<div v-if="expand">
<div
v-for="itemType in itemTypes"
:key="itemType"
@@ -227,11 +232,14 @@ export default {
},
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
this.hero.purchasedPath = item.path;
this.hero.purchasedVal = item.value;
await this.saveHero({ hero: this.hero, msg: item.path });
await this.saveHero({
hero: {
_id: this.hero._id,
purchasedPath: item.path,
purchasedVal: item.value,
},
msg: item.path,
});
item.modified = false;
},
enableValueChange (item) {

View File

@@ -47,6 +47,11 @@
:preferences="hero.preferences"
/>
<stats
:hero="hero"
:reset-counter="resetCounter"
/>
<items-owned
:hero="hero"
:reset-counter="resetCounter"
@@ -67,6 +72,11 @@
:reset-counter="resetCounter"
/>
<user-history
:hero="hero"
:reset-counter="resetCounter"
/>
<contributor-details
:hero="hero"
:reset-counter="resetCounter"
@@ -121,6 +131,8 @@ import Transactions from './transactions';
import SubscriptionAndPerks from './subscriptionAndPerks';
import CustomizationsOwned from './customizationsOwned.vue';
import Achievements from './achievements.vue';
import UserHistory from './userHistory.vue';
import Stats from './stats.vue';
import { userStateMixin } from '../../../mixins/userState';
@@ -135,6 +147,8 @@ export default {
PrivilegesAndGems,
ContributorDetails,
Transactions,
UserHistory,
Stats,
SubscriptionAndPerks,
UserProfile,
Achievements,

View File

@@ -1,13 +1,18 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Items
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Items
</h3>
<div v-if="expand">
<div>
The sections below display each item's key (bolded if the player has ever owned it),
followed by the item's English name.
@@ -264,16 +269,19 @@ export default {
methods: {
async saveItem (item) {
// prepare the item's new value and path for being saved
this.hero.itemPath = item.path;
const toSave = {
_id: this.hero._id,
};
toSave.itemPath = item.path;
if (item.value === null) {
this.hero.itemVal = 'null';
toSave.itemVal = 'null';
} else if (item.value === false) {
this.hero.itemVal = 'false';
toSave.itemVal = 'false';
} else {
this.hero.itemVal = item.value;
toSave.itemVal = item.value;
}
await this.saveHero({ hero: this.hero, msg: item.key });
await this.saveHero({ hero: toSave, msg: item.key });
item.neverOwned = false;
item.modified = false;
},

View File

@@ -1,16 +1,21 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Party, Quest
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Party, Quest
<span
v-if="errorsOrWarningsExist"
>- ERRORS / WARNINGS EXIST</span>
</h3>
<div v-if="expand">
<div
v-if="errorsOrWarningsExist"
class="errorMessage"

View File

@@ -1,87 +1,138 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Privileges, Gem Balance
</h3>
<div v-if="expand">
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
<form @submit.prevent="saveHero({hero: {
_id: hero._id,
flags: hero.flags,
balance: hero.balance,
auth: hero.auth,
secret: hero.secret,
}, msg: 'Privileges or Gems or Moderation Notes'})">
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Priviliges, Gem Balance
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Player has had privileges removed or has moderation notes.
</p>
<form @submit.prevent="saveHero({hero, msg: 'Privileges or Gems or Moderation Notes'})">
<div class="checkbox">
<label>
<input
v-if="hero.flags"
v-model="hero.flags.chatShadowMuted"
type="checkbox"
> Shadow Mute
</label>
<p
v-if="errorsOrWarningsExist"
class="errorMessage"
>
Player has had privileges removed or has moderation notes.
</p>
<div
v-if="hero.flags"
class="form-group row"
>
<div class="col-sm-9 offset-sm-3">
<div class="custom-control custom-checkbox">
<input
id="chatShadowMuted"
v-model="hero.flags.chatShadowMuted"
class="custom-control-input"
type="checkbox"
>
<label
class="custom-control-label"
for="chatShadowMuted"
>
Shadow Mute
</label>
</div>
</div>
</div>
<div class="checkbox">
<label>
<input
v-if="hero.flags"
v-model="hero.flags.chatRevoked"
type="checkbox"
> Mute (Revoke Chat Privileges)
</label>
<div
v-if="hero.flags"
class="form-group row"
>
<div class="col-sm-9 offset-sm-3">
<div class="custom-control custom-checkbox">
<input
id="chatRevoked"
v-model="hero.flags.chatRevoked"
class="custom-control-input"
type="checkbox"
>
<label
class="custom-control-label"
for="chatRevoked"
>
Mute (Revoke Chat Privileges)
</label>
</div>
</div>
</div>
<div class="checkbox">
<label>
<input
v-model="hero.auth.blocked"
type="checkbox"
> Ban / Block
</label>
<div class="form-group row">
<div class="col-sm-9 offset-sm-3">
<div class="custom-control custom-checkbox">
<input
id="blocked"
v-model="hero.auth.blocked"
class="custom-control-input"
type="checkbox"
>
<label
class="custom-control-label"
for="blocked"
>
Ban / Block
</label>
</div>
</div>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Balance
</label>
<div class="col-sm-9">
<input
v-model="hero.balance"
class="form-control balanceField"
type="number"
step="0.25"
>
</label>
<span>
<small>
Balance is in USD, not in Gems.
E.g., if this number is 1, it means 4 Gems.
Arrows change Balance by 0.25 (i.e., 1 Gem per click).
Do not use when awarding tiers; tier gems are automatic.
</small>
</span>
</div>
</div>
<div class="form-group">
<label>Moderation Notes</label>
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="5"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Moderation Notes</label>
<div class="col-sm-9">
<textarea
v-model="hero.secret.text"
class="form-control"
cols="5"
rows="5"
></textarea>
<div
v-markdown="hero.secret.text"
class="markdownPreview"
></div>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary"
class="btn btn-primary mt-1"
>
</form>
</div>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>

View File

@@ -0,0 +1,72 @@
<template>
<div class="form-group row">
<label
class="col-sm-3 col-form-label"
:class="color"
>{{ label }}</label>
<div class="col-sm-9">
<input
:value="value"
class="form-control"
type="number"
:step="step"
:max="max"
:min="min"
@input="$emit('input', parseInt($event.target.value, 10))"
>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.about-row {
margin-left: 0px;
margin-right: 0px;
}
.red-label {
color: $red_100;
}
.blue-label {
color: $blue_100;
}
.purple-label {
color: $purple_300;
}
.yellow-label {
color: $yellow_50;
}
</style>
<script>
export default {
model: {
prop: 'value',
event: 'input',
},
props: {
label: {
type: String,
required: true,
},
color: {
type: String,
default: 'text-label',
},
value: {
type: Number,
required: true,
},
step: {
type: String,
default: 'any',
},
min: {
},
max: {
},
},
};
</script>

View File

@@ -0,0 +1,235 @@
<template>
<form @submit.prevent="submitClicked()">
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Stats
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<stats-row
label="Health"
color="red-label"
:max="maxHealth"
v-model="hero.stats.hp" />
<stats-row
label="Experience"
color="yellow-label"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.exp" />
<stats-row
label="Mana"
color="blue-label"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.mp" />
<stats-row
label="Level"
step="1"
min="0"
:max="maxLevelHardCap"
v-model="hero.stats.lvl" />
<stats-row
label="Gold"
min="0"
:max="maxFieldHardCap"
v-model="hero.stats.gp" />
<h3>Stat Points</h3>
<stats-row
label="Unallocated"
min="0"
step="1"
:max="maxStatPoints"
v-model="hero.stats.points" />
<stats-row
label="Strength"
color="red-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.str" />
<stats-row
label="Intelligence"
color="blue-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.int" />
<stats-row
label="Perception"
color="purple-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.per" />
<stats-row
label="Constitution"
color="yellow-label"
min="0"
:max="maxStatPoints"
step="1"
v-model="hero.stats.con" />
<div class="form-group row" v-if="statPointsIncorrect">
<div class="offset-sm-3 col-sm-9 red-label">
Error: Sum of stat points should equal the users level
</div>
</div>
<h3>Buffs</h3>
<stats-row
label="Strength"
color="red-label"
min="0"
step="1"
v-model="hero.stats.buffs.str" />
<stats-row
label="Intelligence"
color="blue-label"
min="0"
step="1"
v-model="hero.stats.buffs.int" />
<stats-row
label="Perception"
color="purple-label"
min="0"
step="1"
v-model="hero.stats.buffs.per" />
<stats-row
label="Constitution"
color="yellow-label"
min="0"
step="1"
v-model="hero.stats.buffs.con" />
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<button
type="button"
class="btn btn-warning btn-sm"
@click="resetBuffs">
Reset Buffs
</button>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.about-row {
margin-left: 0px;
margin-right: 0px;
}
</style>
<script>
import {
MAX_HEALTH,
MAX_STAT_POINTS,
MAX_LEVEL_HARD_CAP,
MAX_FIELD_HARD_CAP,
} from '@/../../common/script/constants';
import markdownDirective from '@/directives/markdown';
import saveHero from '../mixins/saveHero';
import { mapState } from '@/libs/store';
import { userStateMixin } from '../../../mixins/userState';
import StatsRow from './stats-row';
function resetData (self) {
self.expand = false;
}
export default {
directives: {
markdown: markdownDirective,
},
components: {
StatsRow,
},
mixins: [
userStateMixin,
saveHero,
],
computed: {
...mapState({ user: 'user.data' }),
statPointsIncorrect () {
return (parseInt(this.hero.stats.points, 10)
+ parseInt(this.hero.stats.str, 10)
+ parseInt(this.hero.stats.int, 10)
+ parseInt(this.hero.stats.per, 10)
+ parseInt(this.hero.stats.con, 10)
) !== this.hero.stats.lvl;
},
},
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
maxHealth: MAX_HEALTH,
maxStatPoints: MAX_STAT_POINTS,
maxLevelHardCap: MAX_LEVEL_HARD_CAP,
maxFieldHardCap: MAX_FIELD_HARD_CAP,
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
submitClicked () {
if (this.statPointsIncorrect) {
return;
}
this.saveHero({
hero: {
_id: this.hero._id,
stats: this.hero.stats,
},
msg: 'Stats',
});
},
resetBuffs () {
this.hero.stats.buffs = {
str: 0,
int: 0,
per: 0,
con: 0,
};
},
},
};
</script>

View File

@@ -1,14 +1,24 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Subscription, Monthly Perks
</h3>
<div v-if="expand">
<form @submit.prevent="saveHero({ hero, msg: 'Subscription Perks' })">
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
purchased: hero.purchased
}, msg: 'Subscription Perks' })"
>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{ 'open': expand }"
@click="expand = !expand"
>
Subscription, Monthly Perks
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div v-if="hero.purchased.plan.paymentMethod">
Payment method:
<strong>{{ hero.purchased.plan.paymentMethod }}</strong>
@@ -23,46 +33,72 @@
</div>
<div
v-if="hero.purchased.plan.dateCreated"
class="form-inline"
class="form-group row"
>
<label>
<label class="col-sm-3 col-form-label">
Creation date:
<input
v-model="hero.purchased.plan.dateCreated"
class="form-control"
type="text"
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateCreated"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateCreated) }}
</strong>
</div>
</div>
</div>
</div>
<div
v-if="hero.purchased.plan.dateCurrentTypeCreated"
class="form-inline"
class="form-group row"
>
<label>
Start date for current subscription type:
<input
v-model="hero.purchased.plan.dateCurrentTypeCreated"
class="form-control"
type="text"
>
<label class="col-sm-3 col-form-label">
Current sub start date:
</label>
<strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateCurrentTypeCreated"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}
</strong>
</div>
</div>
</div>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Termination date:
<div>
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateTerminated"
class="form-control"
type="text"
> <strong class="ml-2">{{ dateFormat(hero.purchased.plan.dateTerminated) }}</strong>
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
</strong>
</div>
</div>
</label>
</div>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Consecutive months:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.count"
class="form-control"
@@ -70,11 +106,13 @@
min="0"
step="1"
>
</label>
</div>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Perk offset months:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.offset"
class="form-control"
@@ -82,26 +120,34 @@
min="0"
step="1"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Perk month count:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
</div>
</div>
<div class="form-inline">
Perk month count:
<input
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Next Mystic Hourglass:
</label>
<strong class="col-sm-9 col-form-label">{{ nextHourglassDate }}</strong>
</div>
<div>
Next Mystic Hourglass:
<strong>{{ nextHourglassDate }}</strong>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Mystic Hourglasses:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.trinkets"
class="form-control"
@@ -109,11 +155,13 @@
min="0"
step="1"
>
</label>
</div>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Gem cap increase:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.gemCapExtra"
class="form-control"
@@ -122,15 +170,21 @@
max="25"
step="5"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Total Gem cap:
</label>
<strong class="col-sm-9 col-form-label">
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}
</strong>
</div>
<div>
Total Gem cap:
<strong>{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}</strong>
</div>
<div class="form-inline">
<label>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Gems bought this month:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.gemsBought"
class="form-control"
@@ -139,43 +193,64 @@
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
step="1"
>
</label>
</div>
</div>
<div
v-if="hero.purchased.plan.extraMonths > 0"
>
<div v-if="hero.purchased.plan.extraMonths > 0">
Additional credit (applied upon cancellation):
<strong>{{ hero.purchased.plan.extraMonths }}</strong>
</div>
<div>
Mystery Items:
<span
v-if="hero.purchased.plan.mysteryItems.length > 0"
>
<span
v-for="(item, index) in hero.purchased.plan.mysteryItems"
:key="index"
>
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
{{ item }},
</strong>
<strong v-else> {{ item }} </strong>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Mystery Items:
</label>
<div class="col-sm-9 col-form-label">
<span v-if="hero.purchased.plan.mysteryItems.length > 0">
<span
v-for="(item, index) in hero.purchased.plan.mysteryItems"
:key="index"
>
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
{{ item }},
</strong>
<strong v-else> {{ item }} </strong>
</span>
</span>
</span>
<span v-else>
<strong>None</strong>
</span>
<span v-else>
<strong>None</strong>
</span>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
>
</form>
</div>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
}
}
</style>
<script>
import moment from 'moment';
import { getPlanContext } from '@/../../common/script/cron';

View File

@@ -1,13 +1,18 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="toggleTransactionsOpen"
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="toggleTransactionsOpen"
>
Transactions
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
Transactions
</h3>
<div v-if="expand">
<purchase-history-table
:gem-transactions="gemTransactions"
:hourglass-transactions="hourglassTransactions"

View File

@@ -0,0 +1,236 @@
<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="toggleHistoryOpen"
>
User History
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div>
<div class="clearfix">
<div class="mb-4 float-left">
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'armoire'}"
@click="selectTab('armoire')"
>
Armoire
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'questInvites'}"
@click="selectTab('questInvites')"
>
Quest Invitations
</button>
<button
class="page-header btn-flat tab-button textCondensed"
:class="{'active': selectedTab === 'cron'}"
@click="selectTab('cron')"
>
Cron
</button>
</div>
</div>
<div class="row">
<div
v-if="selectedTab === 'armoire'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th
v-once
>
Received
</th>
</tr>
<tr
v-for="entry in armoire"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.reward }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'questInvites'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th v-once>
Quest Key
</th>
<th v-once>
Response
</th>
</tr>
<tr
v-for="entry in questInviteResponses"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.quest }}</td>
<td>{{ entry.response }}</td>
</tr>
</table>
</div>
<div
v-if="selectedTab === 'cron'"
class="col-12"
>
<table class="table">
<tr>
<th
v-once
>
{{ $t('timestamp') }}
</th>
<th v-once>
Client
</th>
<th v-once>
Checkin Count
</th>
</tr>
<tr
v-for="entry in cron"
:key="entry.timestamp"
>
<td>
<span
v-b-tooltip.hover="entry.timestamp"
>{{ entry.timestamp | timeAgo }}</span>
</td>
<td>{{ entry.client }}</td>
<td>{{ entry.checkinCount }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.page-header.btn-flat {
background: transparent;
}
.tab-button {
height: 2rem;
font-size: 24px;
font-weight: bold;
font-stretch: condensed;
line-height: 1.33;
letter-spacing: normal;
color: $gray-10;
margin-right: 1.125rem;
padding-left: 0;
padding-right: 0;
padding-bottom: 2.5rem;
&.active, &:hover {
color: $purple-300;
box-shadow: 0px -0.25rem 0px $purple-300 inset;
outline: none;
}
}
</style>
<script>
import moment from 'moment';
import { userStateMixin } from '../../../mixins/userState';
export default {
filters: {
timeAgo (value) {
return moment(value).fromNow();
},
},
mixins: [userStateMixin],
props: {
hero: {
type: Object,
required: true,
},
resetCounter: {
type: Number,
required: true,
},
},
data () {
return {
expand: false,
selectedTab: 'armoire',
armoire: [],
questInviteResponses: [],
cron: [],
};
},
watch: {
resetCounter () {
if (this.expand) {
this.retrieveUserHistory();
}
},
},
methods: {
selectTab (type) {
this.selectedTab = type;
},
async toggleHistoryOpen () {
this.expand = !this.expand;
if (this.expand) {
this.retrieveUserHistory();
}
},
async retrieveUserHistory () {
const history = await this.$store.dispatch('adminPanel:getUserHistory', { userIdentifier: this.hero._id });
this.armoire = history.armoire;
this.questInviteResponses = history.questInviteResponses;
this.cron = history.cron;
},
},
};
</script>

View File

@@ -1,52 +1,71 @@
<template>
<div class="accordion-group">
<h3
class="expand-toggle"
:class="{'open': expand}"
@click="expand = !expand"
>
Users Profile
</h3>
<div v-if="expand">
<form @submit.prevent="saveHero({hero, msg: 'Users Profile'})">
<div class="form-group">
<label>Display name</label>
<input
v-model="hero.profile.name"
class="form-control textField"
type="text"
>
<form
@submit.prevent="saveHero({hero: {
_id: hero._id,
profile: hero.profile
}, msg: 'Users Profile'})"
>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
User Profile
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Display name</label>
<div class="col-sm-9">
<input
v-model="hero.profile.name"
class="form-control"
type="text"
>
</div>
</div>
<div class="form-group">
<label>Photo URL</label>
<input
v-model="hero.profile.imageUrl"
class="form-control textField"
type="text"
>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Photo URL</label>
<div class="col-sm-9">
<input
v-model="hero.profile.imageUrl"
class="form-control"
type="text"
>
</div>
</div>
<div class="form-group">
<label>About</label>
<div class="row about-row">
<div class="form-group row">
<label class="col-sm-3 col-form-label">About</label>
<div class="col-sm-9">
<textarea
v-model="hero.profile.blurb"
class="form-control col"
class="form-control"
rows="10"
></textarea>
<div
v-markdown="hero.profile.blurb"
class="markdownPreview col"
class="markdownPreview"
></div>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer"
>
<input
type="submit"
value="Save"
class="btn btn-primary"
class="btn btn-primary mt-1"
>
</form>
</div>
</div>
</div>
</form>
</template>
<style lang="scss" scoped>

View File

@@ -291,6 +291,7 @@
</div>
<div
class="time-travel"
v-if="TIME_TRAVEL_ENABLED && user.permissions && user.permissions.fullAccess"
:key="lastTimeJump"
>
@@ -309,9 +310,11 @@
<div class="my-2">
Time Traveling! It is {{ new Date().toLocaleDateString() }}
<a
class="btn btn-warning mr-1"
class="btn btn-warning btn-small"
@click="resetTime()"
>Reset</a>
>
Reset
</a>
</div>
<a
class="btn btn-secondary mr-1"
@@ -399,6 +402,10 @@
tooltip="+1000 to boss quests. 300 items to collection quests"
@click="addQuestProgress()"
>Quest Progress Up</a>
<a
class="btn btn-secondary"
@click="bossRage()"
>+ Boss Rage 😡</a>
<a
class="btn btn-secondary"
@click="makeAdmin()"
@@ -506,6 +513,8 @@ li {
grid-area: debug-pop;
}
.time-travel { grid-area: time-travel;}
footer {
background-color: $gray-500;
color: $gray-50;
@@ -526,7 +535,7 @@ footer {
"donate-text donate-text donate-text donate-button social"
"hr hr hr hr hr"
"copyright copyright melior privacy-terms privacy-terms"
"debug-toggle debug-toggle debug-toggle blank blank";
"time-travel time-travel debug-toggle debug-toggle debug-toggle";
grid-template-columns: repeat(5, 1fr);
grid-template-rows: auto;
@@ -960,6 +969,10 @@ export default {
// @TODO: Notification.text('Quest progress increased');
// @TODO: User.sync();
},
async bossRage () {
await axios.post('/api/v4/debug/boss-rage');
},
async makeAdmin () {
await axios.post('/api/v4/debug/make-admin');
// @TODO: Notification.text('You are now an admin!

View File

@@ -224,7 +224,7 @@
<script>
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import isEmail from 'validator/es/lib/isEmail';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { setUpAxios, buildAppleAuthUrl } from '@/libs/auth';
import googleIcon from '@/assets/svg/google.svg';

View File

@@ -607,7 +607,7 @@
import axios from 'axios';
import hello from 'hellojs';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import isEmail from 'validator/es/lib/isEmail';
import DOMPurify from 'dompurify';
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
import { buildAppleAuthUrl } from '../../libs/auth';

View File

@@ -6,6 +6,7 @@
<div
v-for="option in items"
:key="option.key"
:id="option.imageName"
class="outer-option-background"
:class="{
premium: Boolean(option.gem),
@@ -14,18 +15,28 @@
hide: option.hide }"
@click="option.click(option)"
>
<b-popover
:target="option.imageName"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
>
<strong> {{ option.text }} </strong>
</b-popover>
<div class="option">
<div
class="sprite customize-option"
:class="option.class"
>
<Sprite
v-if="!option.none"
class="sprite"
:prefix="option.isGear ? 'shop' : 'icon'"
:imageName="option.imageName"
:image-name="option.imageName"
/>
<div
v-if="option.none"
v-else
class="redline-outer"
>
<div class="redline"></div>
</div>
</div>
</div>
</div>
</div>
@@ -35,8 +46,12 @@
import gem from '@/assets/svg/gem.svg';
import gold from '@/assets/svg/gold.svg';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
import Sprite from '@/components/ui/sprite.vue';
export default {
components: {
Sprite,
},
mixins: [
avatarEditorUtilities,
],
@@ -75,7 +90,7 @@ export default {
cursor: pointer;
&.premium {
height: 112px;
height: 120px;
width: 96px;
margin-left: 8px;
margin-right: 8px;
@@ -92,21 +107,9 @@ export default {
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
background-color: $white;
.sprite.customize-option.shirt {
margin-left: -3px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.sprite.customize-option.skin {
margin-left: -8px !important;
// otherwise its overriden by the .outer-option-background:not(.none) { rules
}
.option {
border: none;
border-radius: 2px;
padding-left: 6px;
padding-top: 4px;
}
&:hover {
@@ -132,14 +135,14 @@ export default {
}
.redline-outer {
height: 60px;
width: 60px;
height: 68px;
width: 68px;
position: absolute;
bottom: 0;
margin: 0 auto 0 0;
.redline {
width: 60px;
width: 68px;
height: 4px;
display: block;
background: red;
@@ -148,7 +151,6 @@ export default {
top: 0;
margin-top: 30px;
margin-bottom: 20px;
margin-left: -1px;
}
}
}
@@ -164,10 +166,9 @@ export default {
}
.option {
vertical-align: bottom;
height: 64px;
width: 64px;
height: 76px;
width: 76px;
margin: 12px 8px;
border: 4px solid transparent;
border-radius: 10px;
position: relative;
@@ -182,44 +183,6 @@ export default {
.sprite.customize-option {
margin-top: 0;
margin-left: 0;
&.skin {
margin-top: -4px;
margin-left: -4px;
}
&.chair {
margin-left: -1px;
margin-top: -1px;
&.button_chair_black {
// different sprite margin?
margin-top: -3px;
}
&.handleless {
margin-left: -5px;
margin-top: -5px;
}
}
&.color, &.bangs, &.beard, &.flower, &.mustache {
background-position-x: -6px;
background-position-y: -12px;
}
&.hair.base {
background-position-x: -6px;
background-position-y: -4px;
}
&.headAccessory {
margin-top: 0;
margin-left: -4px;
}
&.headband {
margin-top: -6px;
margin-left: -27px;
}
}
}
</style>

View File

@@ -75,6 +75,7 @@
<script>
import appearance from '@/../../common/script/content/appearance';
import upperFirst from 'lodash/upperFirst';
import { subPageMixin } from '../../mixins/subPage';
import { userStateMixin } from '../../mixins/userState';
import { avatarEditorUtilities } from '../../mixins/avatarEditUtilities';
@@ -82,9 +83,6 @@ import customizeBanner from './customize-banner';
import customizeOptions from './customize-options';
import subMenu from './sub-menu';
const freeShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price === 0);
const specialShirtKeys = Object.keys(appearance.shirt).filter(k => appearance.shirt[k].price !== 0);
export default {
components: {
customizeBanner,
@@ -106,17 +104,6 @@ export default {
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
},
chairKeys: ['none', 'black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
specialShirtKeys,
items: [
{
id: 'size',
label: this.$t('size'),
},
{
id: 'shirt',
label: this.$t('shirt'),
},
],
};
},
computed: {
@@ -167,6 +154,7 @@ export default {
];
const noneOption = this.createGearItem(0, 'eyewear', 'base');
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
@@ -178,42 +166,36 @@ export default {
option.active = this.user.preferences.costume
? this.user.items.gear.costume.eyewear === newKey
: this.user.items.gear.equipped.eyewear === newKey;
option.class = `eyewear_special_${key}`;
option.imageName = `eyewear_special_${key}`;
option.isGear = true;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
option.text = this.$t(`eyewearSpecial${upperFirst(key)}Text`);
options.push(option);
}
return options;
},
freeShirts () {
return freeShirtKeys.map(s => this.mapKeysToFreeOption(s, 'shirt'));
},
specialShirts () {
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
const keys = this.specialShirtKeys;
const options = keys.map(key => this.mapKeysToOption(key, 'shirt'));
return options;
},
headbands () {
const keys = ['blackHeadband', 'blueHeadband', 'greenHeadband', 'pinkHeadband', 'redHeadband', 'whiteHeadband', 'yellowHeadband'];
const noneOption = this.createGearItem(0, 'headAccessory', 'base', 'headband');
const noneOption = this.createGearItem(0, 'headAccessory', 'base');
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
for (const key of keys) {
const option = this.createGearItem(key, 'headAccessory', 'special', 'headband');
const option = this.createGearItem(key, 'headAccessory', 'special');
const newKey = `headAccessory_special_${key}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
options.push(option);
}
@@ -227,8 +209,9 @@ export default {
option.none = true;
}
option.active = this.user.preferences.chair === key;
option.class = `button_chair_${key} chair ${key.includes('handleless_') ? 'handleless' : ''}`;
option.imageName = `chair_${key}`;
option.click = () => this.set({ 'preferences.chair': key });
option.text = appearance.chair[key].text();
return option;
});
return options;
@@ -242,8 +225,11 @@ export default {
option.none = true;
}
option.active = this.user.preferences.hair.flower === key;
option.class = `icon_hair_flower_${key} flower`;
if (key !== 0) {
option.imageName = `hair_flower_${key}`;
}
option.click = () => this.set({ 'preferences.hair.flower': key });
option.text = appearance.hair.flower[key].text();
return option;
});
return options;
@@ -271,6 +257,7 @@ export default {
const noneOption = this.createGearItem(0, category, 'base', category);
noneOption.none = true;
noneOption.text = this.$t('none');
const options = [
noneOption,
];
@@ -284,10 +271,15 @@ export default {
option.active = this.user.preferences.costume
? this.user.items.gear.costume[category] === newKey
: this.user.items.gear.equipped[category] === newKey;
option.class = `headAccessory_special_${option.key} ${category}`;
if (category === 'back') {
option.class = `icon_back_special_${option.key} back`;
option.text = this.$t(`back${upperFirst(key)}Text`);
option.imageName = `back_special_${option.key}`;
} else {
option.text = this.$t(`headAccessory${upperFirst(key)}Text`);
option.imageName = `headAccessory_special_${option.key}`;
}
option.isGear = true;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
@@ -303,7 +295,7 @@ export default {
return keys.join(',');
},
createGearItem (key, gearType, subGearType, additionalClass) {
createGearItem (key, gearType, subGearType) {
const newKey = `${gearType}_${subGearType ? `${subGearType}_` : ''}${key}`;
const option = {};
option.key = key;
@@ -311,6 +303,7 @@ export default {
const currentlyEquippedValue = this.user.items.gear[visibleGearType][gearType];
option.active = currentlyEquippedValue === newKey;
option.isGear = true;
if (key === 0) {
// if key is the "none" option check if a property
@@ -318,7 +311,7 @@ export default {
option.active = option.active || !currentlyEquippedValue;
}
option.class = `${newKey} ${additionalClass}`;
option.imageName = `${newKey}`;
option.click = () => {
const type = this.user.preferences.costume ? 'costume' : 'equipped';
const currentlyEquipped = this.user.items.gear[type][gearType];

View File

@@ -167,7 +167,7 @@ label {
<script>
import axios from 'axios';
import isEmail from 'validator/lib/isEmail';
import isEmail from 'validator/es/lib/isEmail';
import closeX from '@/components/ui/closeX';
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';

View File

@@ -220,10 +220,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<div
<Sprite
class="background"
:class="`icon_background_${bg.key}`"
></div>
:image-name="`icon_background_${bg.key}`"
/>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -254,10 +254,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<div
<Sprite
class="background"
:class="`icon_background_${bg.key}`"
></div>
:image-name="`icon_background_${bg.key}`"
/>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -286,10 +286,10 @@
:class="{selected: bg.key === user.preferences.background}"
@click="unlock('background.' + bg.key)"
>
<div
<Sprite
class="background"
:class="`icon_background_${bg.key}`"
></div>
:image-name="`icon_background_${bg.key}`"
/>
<b-popover
:target="bg.key"
triggers="hover focus"
@@ -818,9 +818,10 @@
.background {
border-radius: 4px;
object-position: -4px -4px;
object-fit: none;
width: 60px;
height: 60px;
background-position: -4px -4px;
}
.deselect {
@@ -1013,6 +1014,7 @@ import arrowRight from '@/assets/svg/arrow_right.svg';
import arrowLeft from '@/assets/svg/arrow_left.svg';
import svgClose from '@/assets/svg/close.svg';
import { avatarEditorUtilities } from '../mixins/avatarEditUtilities';
import Sprite from './ui/sprite';
export default {
components: {
@@ -1024,6 +1026,7 @@ export default {
hairSettings,
skinSettings,
usernameForm,
Sprite,
},
mixins: [guide, notifications, avatarEditorUtilities],
data () {

View File

@@ -122,8 +122,8 @@
<script>
import clone from 'lodash/clone';
import debounce from 'lodash/debounce';
import isEmail from 'validator/lib/isEmail';
import isUUID from 'validator/lib/isUUID';
import isEmail from 'validator/es/lib/isEmail';
import isUUID from 'validator/es/lib/isUUID';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import positiveIcon from '@/assets/svg/positive.svg';

View File

@@ -320,7 +320,7 @@
<script>
import each from 'lodash/each';
import * as quests from '@/../../common/script/content/quests';
import { mountInfo, petInfo } from '@/../../common/script/content/stable';
import stable from '@/../../common/script/content/stable';
import content from '@/../../common/script/content';
import gear from '@/../../common/script/content/gear';
import styleHelper from '@/mixins/styleHelper';
@@ -330,6 +330,8 @@ import userLink from '../userLink';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import { userStateMixin } from '../../mixins/userState';
const { mountInfo, petInfo } = stable;
export default {
components: {
userLink,

View File

@@ -16,10 +16,13 @@
class="brand"
aria-label="Habitica"
>
<div
class="logo svg-icon svg color gryphon"
v-html="icons.melior"
></div>
<router-link to="/">
<div
class="logo svg-icon svg color gryphon pl-2 mr-3"
v-html="icons.melior"
></div>
<div class="svg-icon"></div>
</router-link>
<div class="svg-icon"></div>
</b-navbar-brand>
<b-navbar-toggle
@@ -346,15 +349,15 @@
>
<div
v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')"
class="top-menu-icon svg-icon"
class="top-menu-icon svg-icon mr-1"
v-html="icons.hourglasses"
></div>
<span>{{ userHourglasses }}</span>
</div>
<div class="item-with-icon">
<div class="item-with-icon gem">
<a
v-b-tooltip.hover.bottom="$t('gems')"
class="top-menu-icon svg-icon gem"
class="top-menu-icon svg-icon gem mr-2"
:aria-label="$t('gems')"
href="#buy-gems"
@click.prevent="showBuyGemsModal()"
@@ -365,7 +368,7 @@
<div class="item-with-icon gold">
<div
v-b-tooltip.hover.bottom="$t('gold')"
class="top-menu-icon svg-icon"
class="top-menu-icon svg-icon mr-2"
:aria-label="$t('gold')"
v-html="icons.gold"
></div>
@@ -406,6 +409,180 @@ body.modal-open #habitica-menu {
@import '~@/assets/scss/utils.scss';
@import '~@/assets/scss/variables.scss';
.menu-toggle {
border: none;
}
#menu_collapse {
display: flex;
justify-content: space-between;
}
.topbar {
z-index: 1080;
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
min-height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
a {
color: white !important;
}
}
.logo {
color: $white;
height: 32px;
object-fit: contain;
width: 32px;
}
.quick-menu {
display: flex;
margin-left: auto;
}
.currency-tray {
display: flex;
}
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
transition: none;
.topbar-dropdown {
overflow: hidden;
max-height: 0;
.topbar-dropdown-item {
line-height: 1.5;
font-size: 16px;
}
}
>a {
padding: .8em 1em !important;
}
&.down {
color: $white !important;
background: $purple-200;
.topbar-dropdown {
margin-top: 0; // Remove gap between navbar and drop-down.
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.topbar-dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
display: list-item;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
text-decoration: none;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
}
}
}
.dropdown + .dropdown {
margin-left: 0px;
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
white-space: nowrap;
span {
font-weight: bold;
}
&.gem {
margin-left: 12px;
}
&.gold {
margin-left: 12px;
margin-right: 36px;
}
&:focus ::v-deep .top-menu-icon.svg-icon,
&:hover ::v-deep .top-menu-icon.svg-icon {
color: $white;
}
& ::v-deep .top-menu-icon.svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 24px;
height: 24px;
margin-right: 12px;
margin-left: 12px;
}
}
a.item-with-icon:focus {
outline: none;
}
@keyframes rotateGemColors {
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
20% {
fill: #46A7D9; /* Blue */
}
40% {
fill: #925CF3; /* Purple */
}
60% {
fill: #DE3F3F; /* Red */
}
80% {
fill: #FA8537; /* Orange */
}
100% {
fill: #FFB445; /* Yellow */
}
}
.gem:hover {
cursor: pointer;
& ::v-deep path:nth-child(1) {
animation: rotateGemColors 3s linear infinite alternate;
}
}
.message-count.top-count {
background-color: $red-50;
position: absolute;
right: 0;
top: -0.5em;
padding: .2em;
}
@media only screen and (max-width: 1200px) {
.chevron {
display: none
@@ -413,12 +590,13 @@ body.modal-open #habitica-menu {
.gryphon {
background-size: cover;
height: 32px;
color: $white;
height: 32px;
margin: 0 auto;
width: 32px;
top: -10px;
padding-left: 8px;
position: relative;
width: 32px;
}
.logo {
@@ -542,193 +720,23 @@ body.modal-open #habitica-menu {
.desktop-only {
display: none !important;
}
}
.menu-toggle {
border: none;
}
#menu_collapse {
display: flex;
justify-content: space-between;
}
.topbar {
z-index: 1080;
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
min-height: 56px;
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
a {
color: white !important;
}
}
.logo {
color: $white;
height: 32px;
object-fit: contain;
width: 32px;
}
.quick-menu {
display: flex;
margin-left: auto;
}
.currency-tray {
display: flex;
}
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
transition: none;
.topbar-dropdown {
overflow: hidden;
max-height: 0;
.topbar-dropdown-item {
line-height: 1.5;
font-size: 16px;
}
.navbar-toggler {
padding-left: 8px;
padding-right: 8px;
}
>a {
padding: .8em 1em !important;
}
.item-with-icon {
margin-left: 0px;
margin-right: 16px;
&.down {
color: $white !important;
background: $purple-200;
.topbar-dropdown {
margin-top: 0; // Remove gap between navbar and drop-down.
background: $purple-200;
border-radius: 0px;
border: none;
box-shadow: none;
padding: 0px;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.topbar-dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
display: list-item;
&.active {
background: $purple-300;
}
&:hover {
background: $purple-300;
text-decoration: none;
&:last-child {
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
}
}
}
& ::v-deep .top-menu-icon.svg-icon {
margin-right: 0px;
margin-left: 0px;
}
}
}
.dropdown + .dropdown {
margin-left: 0px;
}
.item-with-icon {
color: $white;
font-size: 16px;
font-weight: normal;
white-space: nowrap;
span {
font-weight: bold;
}
&.gold {
margin-right: 24px;
}
&:focus ::v-deep .top-menu-icon.svg-icon,
&:hover ::v-deep .top-menu-icon.svg-icon {
color: $white;
}
& ::v-deep .top-menu-icon.svg-icon {
color: $header-color;
vertical-align: bottom;
display: inline-block;
width: 24px;
height: 24px;
margin-right: 12px;
margin-left: 12px;
}
}
a.item-with-icon:focus {
outline: none;
}
.menu-icon {
margin-left: 24px;
}
@keyframes rotateGemColors {
/* Gems are green by default, so we rotate through ROYGBIV starting with green. */
20% {
fill: #46A7D9; /* Blue */
}
40% {
fill: #925CF3; /* Purple */
}
60% {
fill: #DE3F3F; /* Red */
}
80% {
fill: #FA8537; /* Orange */
}
100% {
fill: #FFB445; /* Yellow */
}
}
.gem:hover {
cursor: pointer;
& ::v-deep path:nth-child(1) {
animation: rotateGemColors 3s linear infinite alternate;
}
}
.message-count {
background-color: $blue-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
}
.message-count.top-count {
background-color: $red-50;
position: absolute;
right: 0;
top: -0.5em;
padding: .2em;
}
</style>
<script>

View File

@@ -12,13 +12,13 @@
.message-count {
background-color: $red-50;
border-radius: 50%;
height: 20px;
width: 20px;
float: right;
color: $white;
text-align: center;
font-weight: bold;
font-size: 12px;
font-weight: bold;
height: 20px;
left: 24px;
text-align: center;
width: 20px;
svg {
width: 12px;
@@ -36,4 +36,11 @@
.message-count.top-count-gray {
background-color: $gray-200;
}
@media only screen and (max-width: 992px) {
.message-count {
left: 12px;
}
}
</style>

View File

@@ -14,7 +14,7 @@
:top="true"
/>
<div
class="top-menu-icon svg-icon user"
class="top-menu-icon svg-icon mr-2"
v-html="icons.user"
></div>
</div>
@@ -105,6 +105,11 @@
<style lang='scss' scoped>
@import '~@/assets/scss/colors.scss';
@media only screen and (max-width: 992px) {
.item-with-icon.item-user {
margin-right: 0px;
}
}
.user-dropdown {
width: 14.75em;

View File

@@ -21,10 +21,10 @@
<slot
name="itemBadge"
:item="item"
></slot><span
></slot><Sprite
class="item-content"
:class="itemContentClass"
></span>
:image-name="itemContentClass"
/>
</div><span
v-if="label"
class="item-label"
@@ -46,8 +46,12 @@
<script>
import { v4 as uuid } from 'uuid';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
props: {
item: {
type: Object,

View File

@@ -0,0 +1,113 @@
<template>
<div
ref="root"
v-if="draggedItem"
class="draggedItemInfo mouse"
v-mousePosition="30"
@mouseMoved="mouseMoved($event)">
<Sprite
class="dragging-icon"
:image-name="imageName()"
/>
<div class="popover">
<div
class="popover-content"
>
{{ $t(popoverTextKey, { [translationKey]: itemText() }) }}
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.draggedItemInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.dragging-icon {
width: 68px;
margin: 0 auto 8px;
display: block;
transform: scale(1.5);
}
.popover {
position: static;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
</style>
<script>
import Sprite from '@/components/ui/sprite';
import MouseMoveDirective from '@/directives/mouseposition.directive';
export default {
name: 'ItemPopover',
components: {
Sprite,
},
directives: {
mousePosition: MouseMoveDirective,
},
props: {
draggedItem: {
type: Object,
default: null,
},
popoverTextKey: {
type: String,
default: '',
},
translationKey: {
type: String,
default: '',
},
},
methods: {
imageName () {
if (this.draggedItem) {
if (this.draggedItem.class) {
return this.draggedItem.class;
}
if (this.draggedItem.target) {
return `Pet_Food_${this.draggedItem.key}`;
}
}
return '';
},
mouseMoved ($event) {
if (this.$refs.root) {
this.$refs.root.style.left = `${$event.x - 60}px`;
this.$refs.root.style.top = `${$event.y + 10}px`;
}
},
itemText () {
if (this.draggedItem) {
if (this.draggedItem.text) {
if (typeof this.draggedItem.text === 'function') {
return this.draggedItem.text();
}
return this.draggedItem.text;
}
return this.draggedItem.class;
}
return '';
},
},
};
</script>

View File

@@ -1,8 +1,6 @@
<template>
<div
v-mousePosition="30"
class="row"
@mouseMoved="mouseMoved($event)"
>
<div class="standard-sidebar d-none d-sm-block">
<filter-sidebar>
@@ -99,7 +97,7 @@
{{ context.item.text }}
</h4>
<div
v-if="currentDraggingPotion == null"
v-if="!currentDraggingPotion"
class="popover-content-text"
>
{{ context.item.notes }}
@@ -148,7 +146,7 @@
<h4 class="popover-content-title">
{{ context.item.text }}
</h4>
<div class="popover-content-text">
<div class="popover-content-text" v-if="!currentDraggingEgg">
{{ context.item.notes }}
</div>
</template>
@@ -224,120 +222,24 @@
</div>
</div>
<hatchedPetDialog />
<div
ref="draggingEggInfo"
class="eggInfo"
>
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div class="popover-content">
{{ $t('dragThisEgg', {eggName: currentDraggingEgg.text }) }}
</div>
</div>
</div>
</div>
<div
v-if="eggClickMode"
ref="clickEggInfo"
class="eggInfo mouse"
>
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnPotionToHatch', {eggName: currentDraggingEgg.text }) }}
</div>
</div>
</div>
</div>
<div
ref="draggingPotionInfo"
class="hatchingPotionInfo"
>
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('dragThisPotion', {potionName: currentDraggingPotion.text }) }}
</div>
</div>
</div>
</div>
<div
v-if="potionClickMode"
ref="clickPotionInfo"
class="hatchingPotionInfo mouse"
>
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnEggToHatch', {potionName: currentDraggingPotion.text }) }}
</div>
</div>
</div>
</div>
<ItemPopover
:dragged-item="currentDraggingEgg"
popoverTextKey="clickOnPotionToHatch"
translationKey="eggName" />
<ItemPopover
:dragged-item="currentDraggingPotion"
popoverTextKey="clickOnEggToHatch"
translationKey="potionName" />
<questDetailModal :group="user.party" />
<cards-modal :card-options="cardOptions" />
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.eggInfo, .hatchingPotionInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.potion-icon {
margin: 0 auto 8px;
transform: scale(1.5);
}
.popover {
position: inherit;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
</style>
<script>
import each from 'lodash/each';
import throttle from 'lodash/throttle';
import moment from 'moment';
import ItemPopover from '@/components/inventory/itemPopover';
import Item from '@/components/inventory/item';
import ItemRows from '@/components/ui/itemRows';
import CountBadge from '@/components/ui/countBadge';
@@ -354,7 +256,6 @@ import { createAnimal } from '@/libs/createAnimal';
import notifications from '@/mixins/notifications';
import DragDropDirective from '@/directives/dragdrop.directive';
import MouseMoveDirective from '@/directives/mouseposition.directive';
import FilterGroup from '@/components/ui/filterGroup';
import Checkbox from '@/components/ui/checkbox';
import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
@@ -375,8 +276,6 @@ const groups = [
allowedItems,
}));
let lastMouseMoveEvent = {};
export default {
name: 'Items',
components: {
@@ -391,10 +290,10 @@ export default {
cardsModal,
QuestInfo,
FilterSidebar,
ItemPopover,
},
directives: {
drag: DragDropDirective,
mousePosition: MouseMoveDirective,
},
mixins: [notifications],
data () {
@@ -405,9 +304,7 @@ export default {
sortBy: 'quantity', // or 'AZ'
currentDraggingEgg: null,
eggClickMode: false,
currentDraggingPotion: null,
potionClickMode: false,
cardOptions: {
cardType: '',
messageOptions: 0,
@@ -567,22 +464,13 @@ export default {
}
this.currentDraggingPotion = null;
this.potionClickMode = false;
return;
}
if (this.currentDraggingEgg === null || this.currentDraggingEgg !== egg) {
this.currentDraggingEgg = egg;
this.eggClickMode = true;
// Wait for the div.eggInfo.mouse node to be added to the DOM before
// changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
} else {
this.currentDraggingEgg = null;
this.eggClickMode = false;
}
},
onPotionClicked ($event, potion) {
@@ -592,21 +480,12 @@ export default {
}
this.currentDraggingEgg = null;
this.eggClickMode = false;
return;
}
if (this.currentDraggingPotion === null || this.currentDraggingPotion !== potion) {
this.currentDraggingPotion = potion;
this.potionClickMode = true;
// Wait for the div.hatchingPotionInfo.mouse node to be added to the
// DOM before changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
} else {
this.currentDraggingPotion = null;
this.potionClickMode = false;
}
},
@@ -640,23 +519,6 @@ export default {
});
}
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged potion/egg info on item click.
lastMouseMoveEvent = $event;
// Update the potion/egg popover if we are already dragging it.
if (this.potionClickMode) {
// dragging potioninfo is 180px wide (90 would be centered)
this.$refs.clickPotionInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickPotionInfo.style.top = `${$event.y + 10}px`;
} else if (this.eggClickMode) {
// dragging eggInfo is 180px wide (90 would be centered)
this.$refs.clickEggInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickEggInfo.style.top = `${$event.y + 10}px`;
}
},
},
};
</script>

View File

@@ -13,13 +13,13 @@
:show="true"
:count="itemCount"
/>
<span
<Sprite
v-drag.food="item.key"
class="item-content"
:class="`Pet_Food_${item.key}`"
:image-name="`Pet_Food_${item.key}`"
@itemDragEnd="dragend($event)"
@itemDragStart="dragstart($event)"
></span>
/>
</div>
</div>
<b-popover
@@ -41,12 +41,14 @@
<script>
import { v4 as uuid } from 'uuid';
import DragDropDirective from '@/directives/dragdrop.directive';
import Sprite from '@/components/ui/sprite';
import CountBadge from '@/components/ui/countBadge';
export default {
components: {
CountBadge,
Sprite,
},
directives: {
drag: DragDropDirective,

View File

@@ -20,7 +20,7 @@
</div>
<div class="inner-content">
<div class="pet-background d-flex align-items-center">
<div :class="pet.class"></div>
<Sprite :image-name="pet.imageName" />
</div>
<h4 class="title">
{{ pet.name }}
@@ -76,10 +76,11 @@
height: 112px;
border-radius: 4px;
background-color: $gray-700;
}
.Pet {
margin: auto;
img {
transform: scale(1.5);
margin: auto;
}
}
.dialog-header {
@@ -103,8 +104,12 @@
<script>
import markdownDirective from '@/directives/markdown';
import svgClose from '@/assets/svg/close.svg';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
directives: {
markdown: markdownDirective,
},

View File

@@ -6,10 +6,10 @@
>
<div class="potionEggGroup">
<div class="potionEggBackground">
<div :class="`Pet_HatchingPotion_${hatchablePet.potionKey}`"></div>
<Sprite :image-name="`Pet_HatchingPotion_${hatchablePet.potionKey}`" />
</div>
<div class="potionEggBackground">
<div :class="`Pet_Egg_${hatchablePet.eggKey}`"></div>
<Sprite :image-name="`Pet_Egg_${hatchablePet.eggKey}`" />
</div>
</div>
<h4 class="title">
@@ -105,7 +105,7 @@
margin-right: 24px;
}
div {
img {
margin: 0 auto;
}
}
@@ -116,8 +116,12 @@
import svgClose from '@/assets/svg/close.svg';
import petMixin from '@/mixins/petMixin';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
mixins: [petMixin],
props: ['hatchablePet'],
data () {

View File

@@ -1,8 +1,6 @@
<template>
<div
v-mousePosition="30"
class="row stable"
@mouseMoved="mouseMoved($event)"
>
<div class="standard-sidebar d-none d-sm-block">
<filter-sidebar>
@@ -265,43 +263,10 @@
</inventoryDrawer>
</div>
<hatchedPetDialog :hide-text="true" />
<div
ref="dragginFoodInfo"
class="foodInfo"
>
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('dragThisFood', {foodName: currentDraggingFood.text() }) }}
</div>
</div>
</div>
</div>
<div
v-if="foodClickMode"
ref="clickFoodInfo"
class="foodInfo mouse"
>
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
class="popover-content"
>
{{ $t('clickOnPetToFeed', {foodName: currentDraggingFood.text() }) }}
</div>
</div>
</div>
</div>
<ItemPopover
:dragged-item="currentDraggingFood"
popoverTextKey="clickOnPetToFeed"
translationKey="foodName" />
<mount-raised-modal />
<welcome-modal />
<hatching-modal :hatchable-pet.sync="hatchablePet" />
@@ -364,34 +329,6 @@
margin-bottom: 0;
}
.foodInfo {
position: absolute;
left: -500px;
z-index: 1080;
&.mouse {
position: fixed;
pointer-events: none
}
.food-icon {
margin: 0 auto 8px;
transform: scale(1.5);
}
.popover {
position: inherit;
width: 180px;
}
.popover-content {
color: white;
margin: 15px;
text-align: center;
}
}
.hatchablePopover {
width: 180px;
@@ -428,6 +365,7 @@ import _throttle from 'lodash/throttle';
import groupBy from 'lodash/groupBy';
import { mapState } from '@/libs/store';
import ItemPopover from '@/components/inventory/itemPopover';
import PetItem from './petItem';
import MountItem from './mountItem.vue';
import FoodItem from './foodItem';
@@ -440,7 +378,6 @@ import InventoryDrawer from '@/components/shared/inventoryDrawer';
import ResizeDirective from '@/directives/resize.directive';
import DragDropDirective from '@/directives/dragdrop.directive';
import MouseMoveDirective from '@/directives/mouseposition.directive';
import { createAnimal } from '@/libs/createAnimal';
@@ -482,11 +419,11 @@ export default {
WelcomeModal,
HatchingModal,
InventoryDrawer,
ItemPopover,
},
directives: {
resize: ResizeDirective,
drag: DragDropDirective,
mousePosition: MouseMoveDirective,
},
mixins: [notifications, openedItemRowsMixin, petMixin, seasonalNPC],
data () {

View File

@@ -13,10 +13,11 @@
name="itemBadge"
:item="item"
></slot>
<span
<Sprite
class="item-content"
:class="itemClass()"
></span>
:image-name="imageName()"
/>
</div>
</div>
<b-popover
@@ -37,8 +38,12 @@
import { v4 as uuid } from 'uuid';
import { mapState } from '@/libs/store';
import { isOwned } from '../../../libs/createAnimal';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
props: {
item: {
type: Object,
@@ -70,7 +75,10 @@ export default {
return isOwned('mount', this.item, this.userItems);
},
itemClass () {
return this.isOwned() ? `Mount_Icon_${this.item.key}` : 'PixelPaw GreyedOut';
return this.isOwned() ? '' : 'GreyedOut';
},
imageName () {
return this.isOwned() ? `stable_Mount_Icon_${this.item.key}` : 'PixelPaw';
},
},
};

View File

@@ -12,10 +12,10 @@
</div>
<div class="inner-content">
<div class="pet-background">
<div
<Sprite
class="mount"
:class="`Mount_Icon_${mount.key}`"
></div>
:image-name="`Mount_Icon_${mount.key}`"
/>
</div>
<h4 class="title">
{{ mount.text() }}
@@ -80,10 +80,14 @@
</style>
<script>
import { mountInfo } from '@/../../common/script/content/stable';
import stable from '@/../../common/script/content/stable';
import markdownDirective from '@/directives/markdown';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
directives: {
markdown: markdownDirective,
},
@@ -105,7 +109,7 @@ export default {
},
methods: {
openDialog (mountKey) {
this.mount = mountInfo[mountKey];
this.mount = stable.mountInfo[mountKey];
this.$root.$emit('bv::show::modal', 'mount-raised-modal');
},
close () {

View File

@@ -13,19 +13,23 @@
name="itemBadge"
:item="item"
></slot><span
v-if="mountOwned() && isHatchable() && !item.isSpecial()"
v-if="isHatchable() && !item.isSpecial()"
class="item-content hatchAgain"
><span
><Sprite
class="egg"
:class="eggClass"
></span><span
:image-name="eggClass"
/><Sprite
class="potion"
:class="potionClass"
></span></span><span
v-else
:image-name="potionClass"
/>
</span>
<Sprite
v-else
class="item-content"
:class="getPetItemClass()"
></span><span
:class="itemClass()"
:image-name="imageName()"
/>
<span
v-if="isAllowedToFeed() && progress() > 0"
class="pet-progress-background"
><div
@@ -52,9 +56,9 @@
v-html="$t('haveHatchablePet', { potion: item.potionName, egg: item.eggName })"
></div><div class="potionEggGroup">
<div class="potionEggBackground">
<div :class="potionClass"></div>
<Sprite :image-name="potionClass" />
</div><div class="potionEggBackground">
<div :class="eggClass"></div>
<Sprite :image-name="eggClass" />
</div>
</div>
</div><div v-else>
@@ -118,8 +122,12 @@ import foolPet from '@/mixins/foolPet';
import {
isAllowedToFeed, isHatchable, isOwned, isSpecial,
} from '../../../libs/createAnimal';
import Sprite from '@/components/ui/sprite';
export default {
components: {
Sprite,
},
mixins: [foolPet],
props: {
item: {
@@ -168,22 +176,28 @@ export default {
isAllowedToFeed () {
return isAllowedToFeed(this.item, this.userItems);
},
getPetItemClass () {
itemClass () {
if (this.isOwned() || this.isHatchable()) {
return '';
}
return 'GreyedOut';
},
imageName () {
if (this.isOwned() && some(
this.currentEventList,
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'Fungi',
)) {
if (this.isSpecial()) return `Pet ${this.foolPet(this.item.key)}`;
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key)}`;
const petString = `${this.item.eggKey}-${this.item.key}`;
return `Pet ${this.foolPet(petString)}`;
return `stable_${this.foolPet(petString)}`;
}
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
return `Pet Pet-${this.item.key} ${this.item.eggKey}`;
return `stable_Pet-${this.item.key}`;
}
if (!this.isOwned() && this.isSpecial()) {
return 'GreyedOut PixelPaw';
return 'PixelPaw';
}
if (this.isHatchable()) {
@@ -191,11 +205,11 @@ export default {
}
if (this.mountOwned()) {
return `GreyedOut Pet Pet-${this.item.key} ${this.item.eggKey}`;
return `stable_Pet-${this.item.key}`;
}
// Can't hatch
return 'GreyedOut PixelPaw';
return 'PixelPaw';
},
progress () {
return this.userItems.pets[this.item.key];

View File

@@ -56,11 +56,11 @@
class="list-group-item"
ng-init="inv.gear[item.key] = user.items.gear.owned[item.key]"
>
<div
<Sprite
class="pull-left"
:class="'shop_' + item.key"
:imageName="'shop_' + item.key"
style="margin-right: 10px"
></div>
/>
{{ item.text() }}
<div class="clearfix">
<label class="radio-inline">
@@ -330,9 +330,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<div
<Sprite
class="pull-left"
:class="'Mount_Icon_' + mount"
:imageName="mount.key"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -363,9 +363,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<div
<Sprite
class="pull-left"
:class="'Mount_Icon_' + mount"
:imageName="mount.key"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -396,9 +396,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<div
<Sprite
class="pull-left"
:class="'Mount_Icon_' + mount"
:imageName="mount.key"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -429,9 +429,9 @@
class="list-group-item"
ng-init="inv.mounts[mount] = user.items.mounts[mount]"
>
<div
<Sprite
class="pull-left"
:class="'Mount_Icon_' + mount"
:imageName="mount.key"
style="margin-right: 10px"
></div>
{{ mount }}
@@ -503,11 +503,11 @@
ng-init="inv.hatchingPotions[item.key] = user.items.hatchingPotions[item.key]"
>
<div class="form-inline clearfix">
<div
<Sprite
class="pull-left"
:class="'Pet_HatchingPotion_' + item.key"
style="margin-right: 10px"
></div>
/>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -565,11 +565,11 @@
ng-init="inv.eggs[item.key] = user.items.eggs[item.key]"
>
<div class="form-inline clearfix">
<div
<Sprite
class="pull-left"
:class="'Pet_Egg_' + item.key"
:image-name="'Pet_Egg_' + item.key"
style="margin-right: 10px"
></div>
/>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -627,11 +627,11 @@
ng-init="inv.food[item.key] = user.items.food[item.key]"
>
<div class="form-inline clearfix">
<div
<Sprite
class="pull-left"
:class="'Pet_Food_' + item.key"
style="margin-right: 10px"
></div>
/>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -690,11 +690,11 @@
ng-if="item.category !== 'world'"
>
<div class="form-inline clearfix">
<div
<Sprite
class="pull-left"
:class="'inventory_quest_scroll_' + item.key"
style="margin-right: 10px"
></div>
/>
<p>{{ item.text() }}</p>
<input
class="form-control"
@@ -730,9 +730,13 @@
import axios from 'axios';
import Content from '@/../../common/script/content';
import Sprite from '@/components/ui/sprite.vue';
import { mapState } from '@/libs/store';
export default {
components: {
Sprite,
},
data () {
const showInv = {};
const inv = {

View File

@@ -295,7 +295,7 @@ h2 {
// import { nextTick } from 'vue'; // may not need this? I don't know!
import debounce from 'lodash/debounce';
import find from 'lodash/find';
import isUUID from 'validator/lib/isUUID';
import isUUID from 'validator/es/lib/isUUID';
import moment from 'moment';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';

View File

@@ -46,10 +46,10 @@
</div>
<div class="row">
<div class="col-2">
<div
:class="currentMysterySet"
<Sprite
:image-name="currentMysterySet"
class="mt-n1"
></div>
/>
</div>
<div class="col-10">
<h3> {{ $t('monthlyMysteryItems') }} </h3>
@@ -628,6 +628,7 @@ import paymentsMixin from '../../mixins/payments';
import notificationsMixin from '../../mixins/notifications';
import subscriptionOptions from './subscriptionOptions.vue';
import Sprite from '@/components/ui/sprite';
import amazonPayLogo from '@/assets/svg/amazonpay.svg';
import applePayLogo from '@/assets/svg/apple-pay-logo.svg';
@@ -648,6 +649,7 @@ import subscriberHourglasses from '@/assets/svg/subscriber-hourglasses.svg';
export default {
components: {
subscriptionOptions,
Sprite,
},
mixins: [paymentsMixin, notificationsMixin],
data () {

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