Squashed commit of the following:

commit 16d8b87e90655c5444d0138e3c9df432b6dab8d9
Merge: 07387faf48 6bea232d47
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Sep 14 22:30:00 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 07387faf48c0060c493904a3d4639a20e6358b62
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Sep 13 23:38:37 2023 +0200

    remove generate promoCode from ui

commit 6bea232d47
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Sep 11 12:55:31 2023 -0400

    build(deps): bump core-js from 3.32.1 to 3.32.2 in /website/client (#14867)

    Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.32.1 to 3.32.2.
    - [Release notes](https://github.com/zloirock/core-js/releases)
    - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/zloirock/core-js/commits/v3.32.2/packages/core-js)

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

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

commit cebb3f0f25
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Sep 11 12:43:49 2023 -0400

    build(deps): bump webpack from 4.46.0 to 4.47.0 in /website/client (#14868)

    Bumps [webpack](https://github.com/webpack/webpack) from 4.46.0 to 4.47.0.
    - [Release notes](https://github.com/webpack/webpack/releases)
    - [Commits](https://github.com/webpack/webpack/compare/v4.46.0...v4.47.0)

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

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

commit ea8563cd17bb2a60c74eb518bbce105b66a8ecd5
Merge: 3e16584dcf 6259955891
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 21:23:02 2023 +0200

    Merge remote-tracking branch 'origin/negue/ui/setting' into negue/ui/setting

commit 3e16584dcf70dc844d806f5f487bf94c171f91ad
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 21:22:06 2023 +0200

    fix PR comments

commit 84ba44fb192c1e6ccd895beae067468285f438a3
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 20:38:54 2023 +0200

    fix PR comments

commit 6259955891a6469760768dcc5e6ae204fa3b44bd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 11:20:26 2023 -0400

    update form.scss

commit da82bd8e68fb1d6567b987a61c2d15176ffa81c2
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Aug 24 21:40:02 2023 +0200

    remove ending

commit 82e5fd2a83e9df85e0268e0937dd83be62bcc73d
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 22:25:41 2023 +0200

    fix spacing

commit 9ad06ea88bbb6b806823184a370366392ff7ad01
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 22:09:22 2023 +0200

    clean up debug row for login methods

commit 41cde37675d6132425b3bb4bb0ac5badb9ba0447
Merge: 8c568060f9 82ebe71eb4
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 21:51:22 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 8c568060f93bf43b3dc82e1e02944bc4b86d5cce
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 21:49:31 2023 +0200

    fix PR comments

commit 36f7a4711da5a9da0e39d284500572f38a57d383
Merge: d279af7897 647b27c55f
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Aug 11 20:04:15 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit d279af7897179dd9c020a795f535dcd25b50dfc0
Merge: ffbed3e044 b20ea44d49
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Aug 9 21:13:37 2023 +0200

    Merge branch 'negue/refactor/routes' into negue/ui/setting

commit b20ea44d495fe9675360856a6f30c39cd9d3a1bd
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Aug 9 21:04:12 2023 +0200

    Split Vue.Router routes

commit ffbed3e0447fb9e3475a21d67ddb014e5b05aabf
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 23 00:00:24 2023 +0200

    remove console

commit 4c350b01803797308bca78970077e9031175529d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 23:34:20 2023 +0200

    update Bailey Notification Text + fix popover

commit c105b9ecf97dda77ec5374f5bec21dff39efb161
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 23:21:53 2023 +0200

    fix change password setting

commit 06410b4807710af4cbb0ee714c692484a8d003fd
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 22:50:00 2023 +0200

    fix reset account texts

commit ccfdd9bb9c4217dbbd6c9cd0677e9125daf0daa9
Merge: 35c75304f1 8558dcc3a8
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 22:48:13 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 35c75304f1ff8ee317499c177a7ddd840468952e
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 2 20:16:06 2023 +0200

    more fixes

commit 203e961464330eeb232e28773e2f57869be0507e
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 2 19:45:17 2023 +0200

    fix notification settings

commit ec946047918bfbd9976529792a7251b934761afb
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 22:00:45 2023 +0200

    applied same styling to promoCode.vue

commit 0177b3a76b140157d999b8d7e1d26c90efd79fc4
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:41:05 2023 +0200

    move promoCode.vue to pages/settings

commit 8fbb600273d3e52a39ae75d277067d64e526f5a9
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:40:35 2023 +0200

    saveCancelButtons.vue allow to hide the cancel part

commit 4915f2a3fb0b7b7747b7a98fbb69d2270c41b418
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:09:07 2023 +0200

    Hide Transactions Page again

commit 8b5ae17f02095cf85988fadc4fd1e36dc26f4005
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:52:03 2023 +0200

    also check for invalid arguments in the password settings

commit aa97ed5299de6dd2dd19182a5cd2fd2c24705d2d
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:25:53 2023 +0200

    fix localhost externalLinks check

commit 87a4e4931bd0230b3af07574c7e2771c87becf2f
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:01:31 2023 +0200

    show notification on username change + fix userEmail checks

commit 6a6f55f6fcdb69e1ca0dcfb63ea7dc96c764cda9
Merge: f9ff5e5c55 e49d26eacd
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jun 24 22:54:00 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit f9ff5e5c55dbc8e519304308adde14748f878a0a
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 22:41:42 2023 +0200

    check password inputs and mark invalid for "password change" setting

commit 4497514eebcb0ae168b385774ca47da654bb9e71
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:59:21 2023 +0200

    show notification when chaning display name

commit 3232f12f0dc39a71cc05a2d5c4b3f0c465383577
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:55:25 2023 +0200

    check current password valid style in "delete account" and "reset account"

commit 582a2f1304dcad57cb26da6211d81a278c0fcd53
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:27:20 2023 +0200

    mark password field of email setting as invalid on wrong password

commit 8e3b8a962a8da871825b894776e567ea739635fc
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:24:46 2023 +0200

    refactor currentPasswordInput.vue to use validatedTextInput.vue

commit 61521507a49f9a2998668a9ca2a9b2274b6d944d
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 20:20:56 2023 +0200

    fix username setting:
    - unsaved values check
    - @ char must be first in input, otherwise not remove it for checks

commit f74c29a065126e802d1301602d1f7aa2164644f6
Merge: c4b6f0c39c d4a5823916
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 19:54:06 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit c4b6f0c39c0526458faf0e5baed9999dfdacdd8b
Merge: 37eee140ad 6e3a367832
Author: negue <eugen.bolz@gmail.com>
Date:   Fri May 12 22:08:08 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 37eee140ad20f453350b718d5ed149de4905c61b
Author: negue <eugen.bolz@gmail.com>
Date:   Fri May 12 21:57:27 2023 +0200

    delete account without password

commit 48a6801f4e606388cbecca124bc7a3c28a969ac3
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 22:06:29 2023 +0200

    fix duplicate json entry

commit 47a2189f497ac548f1aee184a4ea9faaad29e91f
Merge: a56b4a4457 49f45d27e3
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:48:21 2023 +0200

    Merge remote-tracking branch 'origin/release' into negue/ui/setting

commit a56b4a445734c9d069ab7f476e15fabd644a9127
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:37:31 2023 +0200

    show current class on setting panel

commit 9c973cca2a92918a36d2a7578bae64169a6167a9
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:15:46 2023 +0200

    fix selectDifficulty.vue - refactor selectList.vue

commit 95b37b3ba3f74e9b2b14fdc991f79dee44d68ce9
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 20:45:09 2023 +0200

    migrate restoreValues fix to new setting component

commit 7947b1c67d4324166b3e044a2e880c5104cb0beb
Merge: ad3e4d604a 71e165433a
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 20:41:31 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit ad3e4d604a21dcfad410cd77c31b4fc58cddd6a3
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Apr 29 01:18:25 2023 +0200

    style fixes

commit cea13d5bc3782b6e3f8a7ffae9c74a5da43cca6a
Merge: 73a5e5fcab b159182188
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Apr 28 23:58:09 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 73a5e5fcabccf6a0909d0be5a81d8df95a2fd7ef
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 25 20:51:14 2023 +0200

    style / padding issues

commit 0a10eb32ccbea0963dc35467314d877ed4beb458
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Apr 15 20:54:08 2023 +0200

    fix "setting new password" invalid check

commit a79bec3fa5979df9415ad57421afa05ed701bbfc
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 11 23:15:15 2023 +0200

    add password for other logins

commit 9ff17fd6ddca543846140f5eaf8046d457dd8dbb
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 11 23:05:19 2023 +0200

    "fix values" use keydown event to mark as change

commit 1f470942a95466adb27450600f61992bcb005237
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:19:18 2023 +0200

    delete old api.vue

commit b4904a8b84d273e73014659c5f15821deb7e26a7
Merge: b5da7ccc70 c8b98678d0
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:18:07 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit b5da7ccc70cbad6a3c769f63f6b220a0f8f5e105
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:11:36 2023 +0200

    refactor webhook ui to use save/cancel buttons

commit f49f67ff5cd6e0a46b2343e871bbaa168768f12d
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Apr 5 22:56:37 2023 +0200

    remove unused settings

commit cc73b44b25d1f3dd6507754d2a68652e111d8703
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 29 23:40:30 2023 +0200

    remove advancedCollapsed settings to start it opened

commit e0300e87104f36d3e57809b570cd314319b027d5
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 29 22:58:09 2023 +0200

    remove displayInviteToPartyWhenPartyIs1 setting

commit 1741ddfc6484ae2f7da26050a8453c89e9925229
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 20 23:00:17 2023 +0100

    webhook margins

commit 24a43d027c9847663db4255d2b2cd472c8ce02c7
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 20 22:40:19 2023 +0100

    userid tooltip

commit 42fcb20bc4a06d8d6cb72263481c1127a7fac51d
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:51:10 2023 +0100

    remove balance for choosing class

commit 160848473d5bf24fe3b6bf327ff698584120b18c
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:20:56 2023 +0100

    show real class setting modal if enough gems available

commit f74ba9738dad0171136b8e3c1e5c3eee5a7630a0
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:10:53 2023 +0100

    update apple icon and size

commit bf961bc7283b3ba05011b5d3ca50127da5bc9850
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:59:42 2023 +0100

    Copied API Token Notification

commit 28f0220b4ea889bf841c8ca542eb23af3bd22cab
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:53:33 2023 +0100

    remove blue color of setting links

commit b53ccace9593e32ecd3ed5df28d81b3c899f31fc
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:43:06 2023 +0100

    fix username/email setting input width

commit 1dfa5b275dabe4baf7c887e2eb5c42e864a2e8a9
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:11:32 2023 +0100

    developer mode

commit 776618d2db3d95d29bb8a49171740267b0238861
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:11:52 2023 +0100

    Add new Pause Dailies Setting

commit 576c80af7ef75a8d2ebb2c2ddb34fb4c32eed122
Merge: dec1a1159d 377b152ffd
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:04:05 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit dec1a1159df17325c23718b929edf3244e033284
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:00:52 2023 +0100

    developer mode dummy row

commit 1e80a7d145e6fce8a95a98645728d548cc9d4a51
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Mar 11 00:03:33 2023 +0100

    WIP webhook row

commit cc4bedbe2d40811724a98dd8191bb551af884c70
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 10 20:28:57 2023 +0100

    add spritely login creds message to the new api-row / redirect old url to the new one

commit f9833aa78a2cdd6aa08cfd72aa6ef2e7815f4b62
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 9 02:23:39 2023 +0100

    API Token Row

commit 123c9b9bb1706041b2a73b254a80db6f5b6a07c8
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 6 22:46:50 2023 +0100

    "Your User Data" Row instead of Page

commit 0ade5663ae6d83967876b66075411752c29454c9
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 3 22:43:03 2023 +0100

    userid row

commit b4f2236ab8d54b6d89066abaee4d087d0a92e570
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 3 22:22:32 2023 +0100

    rename folder of setting rows

commit 3b050861c45d8680d62894b652cd60912bbb3480
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 21:11:48 2023 +0100

    move remaining setting to generalSettings.vue - delete site.vue - start with siteData.vue

commit b09298fb0171ccf70e63b56d2a6a9ef054719597
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 20:56:03 2023 +0100

    move taskSettings.vue and add it to the settings list

commit 5ed25066ecaca2a42c7e80163968ec6912d710a5
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 20:06:13 2023 +0100

    size/margin for transactions

commit 25e77cbd9559e75813f2a925cf743b69321ba91c
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 19:52:12 2023 +0100

    move purchaseHistory.vue

commit 8e4e1bcb0f3d960edc1b9dadc71cd9e4be615024
Merge: bb14d09aa4 85c50d50e9
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 19:04:31 2023 +0100

    Merge remote-tracking branch 'origin/negue/ui/setting' into negue/ui/setting

commit 85c50d50e9606b8518584d9924620262a018a520
Author: SabreCat <sabe@habitica.com>
Date:   Thu Feb 16 14:23:27 2023 -0600

    fix(css): remove redundant formatting for a elements

commit bb14d09aa494954f47a6f0cce000aedd6eea4668
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Feb 16 01:34:09 2023 +0100

    remove console

commit 8c5e722c7215928701a01207a29fbe560bcc2c4d
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Feb 16 01:26:43 2023 +0100

    first try with the refactored UI of Login Methods

commit 9c8770051db4ac3acf0c79eb9c10b966590748b6
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 19:13:16 2023 +0100

    fix dayStartAdjustmentSetting.vue for 0 value

commit ee2ff3881b0a914a0858940088e345a2a38a4c25
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:37:46 2023 +0100

    fix color after refactor

commit 121e7485ca075f5a8cf5f5ef5072b8221ba6521a
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:29:00 2023 +0100

    mark audioThemeSetting as changed

commit 98c65700039cd86f571f35fd534b472659b767ff
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:05:55 2023 +0100

    fix ul/li style in resetAccount.vue

commit fed824f7052a971a8f40b55ebc2c3937953b3af2
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 17:49:36 2023 +0100

    fix color of gem price

commit 80365e537d6eeee28c94dc82a0ca4234b5245362
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 17:44:55 2023 +0100

    fix "fixValuesSetting.vue"

commit d3e15c541325d911b9db746246f047231df385a1
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Feb 8 01:06:27 2023 +0100

    open forgot password in new tab

commit 31edec9ec55d77840e8c991fcc53572efc2354d6
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Feb 8 01:03:19 2023 +0100

    move validatedTextInput.vue to shared components + fix check pos/size + input-error cleanup

commit 2adfd8c2593e6a5b75ab2f29b88858a5e9d7a6dc
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 20:19:30 2023 +0100

    hide class setting until level 10

commit 64fb4c0cf9c4cfbca8733966cc72ca7a6c1ab742
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 19:32:40 2023 +0100

    delete old modals (refactored into new settings ui)

commit b5be137a8d072c483d06466773c82c1a469ece51
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 19:27:26 2023 +0100

    enable forgot password link in settings

commit bec75c6e129b8dfe0d429a60e4f92b2562b0843d
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 18:52:54 2023 +0100

    reset account + password required in api

commit 64f7e7a1d9b06fe9bb56a8b729e7c88a3732926e
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 23:22:55 2023 +0100

    fix compile

commit 7ffb5101beca7246f1563d589bb85e14b85367ae
Merge: 2bfb130b92 9f64633a57
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 22:47:05 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 2bfb130b92f350667b9be682e80cb02499e9034b
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 22:44:23 2023 +0100

    remove restore-modal and replace it with the finished fix values setting

commit 89530a133ce8e58228edeb44be34d8b5db80248f
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 18 19:22:36 2023 +0100

    wip fix values

commit 428647fc71e35a9112d69282694da4ce2dbb408d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 14 21:50:22 2023 +0100

    refactor change class to design update + clean up old site.vue settings

commit 1f16819bc140e727d35ac81f97fa9f917602db14
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 11 22:41:05 2023 +0100

    WIP fix values

commit 6fef3d057958e6f79ff369fc68267d84679ad086
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 7 22:51:30 2023 +0100

    check for unsaved changes when pressing cancel

commit bef8a4cdfc79d60548bdb86b73654d11d4a7559c
Merge: 494f32c3e3 c7aadede4d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 7 22:10:53 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 494f32c3e3f68334199846049ee4f12cecb1e011
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 21 00:55:31 2022 +0100

    Class Setting

commit bda210cfbb8b8bb19ba01cd5e87dff859b0f0306
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 23:01:41 2022 +0100

    removes username, email and display name from site.vue

commit 38198d7df6e6dd3fa01c709d29627610373c9d7c
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 22:36:27 2022 +0100

    WIP class setting

commit dddcfa637f5bbb5da9a2a8c3454edcc26b13dee9
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 22:31:36 2022 +0100

    fix styles

commit ce0a5cf97417487b1fb2b7a3aa6b489b61966c0c
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 23:57:07 2022 +0100

    Scroll into opened Setting

commit 7e0a95ddff3a4f12f71ba2aaa1a5c603015a9925
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 23:43:44 2022 +0100

    Audio Theme Setting

commit 9c556662fe98ca3d664a86b5ff669b127de8fc32
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 00:25:30 2022 +0100

    prepare header settings but still hidden

commit 30d8b27534080c55fff532ca2b0cb11a6c9bd713
Merge: a1d1a788b2 580139ff69
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 23:36:36 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit a1d1a788b26eff59d9494905083606ad13994279
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 23:34:33 2022 +0100

    DayStartAdjustmentSetting

commit ddee94a3939c9e36541f1f5ffcc58a0de4c3b93a
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 20:00:12 2022 +0100

    disable reset account button when password empty

commit 30a6db4c2dee2f902fea3c5b8522c0efe8cf230b
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 19:54:21 2022 +0100

    hide & reset previous setting when switching to a different one

commit 78093848d7ee76c7c9be187896ed10fd941491cb
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 7 22:19:15 2022 +0100

    validated text input (in/valid border color + icon)

commit e1b444ea63bf41d33d5d36fc330158aeecd262ae
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 6 22:09:54 2022 +0100

    re-enable box-shadow on hover

commit 96dc4e47aeae0d7c86f756cea9c370c2d0e02c77
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 01:13:47 2022 +0100

    remove console log

commit 69ad07daadfa42635137640c7e4d5df1a7756223
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 01:01:17 2022 +0100

    dateFormatSetting

commit bc11c0cf759eb8f64c6eb0bede7a2b9456b93c5a
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 00:49:24 2022 +0100

    move shared components / mixins

commit 0d1a189c6449f0019c744788b9176b05b6e21039
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 00:44:21 2022 +0100

    language Setting + imports cleanup

commit 29ebd89030eef3810654c5b683c3905dc58f1222
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 27 23:23:02 2022 +0100

    fix icon size + fix display name valid checks

commit 5c7747517b89e24a0dab0330f9c71bd5c3f7c14e
Merge: fd5cbc3026 90b34c4dac
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 27 23:08:35 2022 +0100

    Merge remote-tracking branch 'origin/release' into negue/ui/setting

commit fd5cbc30260d574455ad3014e64c0994accf4f89
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:14:21 2022 +0100

    fix conflicts

commit 49361217b0c55efedb15c154b8c121e68c40d94f
Merge: edb427158f 04e2a39a9f
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:12:38 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit edb427158fd69213eb34c57cd156de1ba08ba8bc
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:03:19 2022 +0100

    disable save button if nothing was changed

commit c7e40e9446f67dbdd8e12ee81d94d069f60278c8
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 22 23:36:37 2022 +0100

    delete account row

commit 4bf740c531b6917a283c7278dc391fd014cbc26d
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 22 23:14:24 2022 +0100

    Shared Modal Visible State

commit d718153717c84bc8bd861e5e7eb303333f22bbbf
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 20 18:06:20 2022 +0100

    resetAccount

commit e25922f8b33f3dad2b13110d6fdbaba2a67d87f0
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 16 23:39:26 2022 +0100

    rename functional components for compiler

commit fdbc2c0eee04146cf7ca8a458b9acc28c01bacbf
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 16 01:44:50 2022 +0100

    password setting row

commit 5fd5e6275aebce235cd7ced7bfc1d57b8b9cd8df
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 15 17:35:44 2022 +0100

    update package-lock.json again

commit 9d742fd9a1ae3b99dbeec48a24007473ae59d696
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 15 17:24:15 2022 +0100

    update package-lock.json

commit cd588e74d5fbbb3a7e0da68f0850dbb542c7aa57
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 02:12:39 2022 +0100

    displayNameSetting.vue

commit 265970c5efd001dbbd4957a122efdd7e8b278f3e
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 02:09:47 2022 +0100

    fix lint

commit a2b510caca7ec873c5ea5a694fec6904779b9085
Merge: 0bae5fbe02 4dca69f14b
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 01:15:02 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 0bae5fbe0234841a4e58a78fcc8aaaa01196853b
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 22:00:34 2022 +0100

    userEmailSetting

commit 23da70fa2e8d759db5222aac659ed12436fecee2
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 20:38:14 2022 +0100

    extract save / cancel buttons and the shared inlineSetting "logic"

commit 82047380f3ff60e293fad8843547665c1128f681
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 20:18:21 2022 +0100

    first setting (username) in the new layout

commit 39150349c7a37108da06aa4928284164197cb355
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 2 21:42:12 2022 +0100

    Working on M1 - will be reverted on full merge

commit f7787b318c43948176b6fb7e29bd971683d33c36
Merge: 4c0ecc9938 53fb28cc48
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 1 14:20:24 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 4c0ecc99380cd67b7ae5c40fb1ff27981c80c4a9
Merge: 2f53613a45 62b4315b3d
Author: negue <negue@users.noreply.github.com>
Date:   Sun Oct 30 12:49:34 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 2f53613a459639f9e9e314a1bd77459225392efa
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Oct 10 22:54:41 2022 +0200

    split routes for ease of dev

commit 390f0fc69df10e90161d7f7bf1cb1e70c637d592
Merge: cf222ee63a 137f7d53dc
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Oct 10 22:50:43 2022 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit cf222ee63af051fbc9d7aee2a1ba7322ae9c56f6
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 23:15:35 2022 +0200

    Update remaining Notification labels

commit f837cce125ad77465c2812615456dcf1c2111371
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 22:45:12 2022 +0200

    move site popup settings to notifications

commit fc5181c3a7670eb97918f4d30c3327a5ed8c090a
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 21:12:24 2022 +0200

    fix styling in notification settings

commit 7b5568ed2343d7c7fc427ba53d9cbb684d0c907c
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Sep 10 16:00:56 2022 +0200

    wip notification settings
This commit is contained in:
SabreCat 2023-10-03 13:30:44 -05:00
parent a9757b2d74
commit a0941ffa84
102 changed files with 6074 additions and 3088 deletions

666
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -36,6 +36,7 @@
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"nodemon": "^2.0.20",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",

View file

@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
type: 'habit',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
type: 'daily',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
type: 'todo',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
type: 'reward',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@ -87,6 +95,26 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not allow to reset if the password is missing', async () => {
await expect(user.post('/user/reset', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('does not allow to reset if the password is wrong', async () => {
await expect(user.post('/user/reset', {
password: 'passdw',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const challenge = await generateChallenge(user, guild);
@ -102,7 +130,9 @@ describe('POST /user/reset', () => {
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await user.put('/user', {
@ -133,7 +163,9 @@ describe('POST /user/reset', () => {
},
});
await hero.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);

View file

@ -126,13 +126,5 @@ describe('shared.ops.addTask', () => {
expect(addTask(user)._editing).not.be.ok;
expect(addTask(user)._edit).to.not.be.ok;
});
it('respects advancedCollapsed preference', () => {
user.preferences.advancedCollapsed = true;
expect(addTask(user)._advanced).not.be.ok;
user.preferences.advancedCollapsed = false;
expect(addTask(user)._advanced).to.be.ok;
});
});
});

View file

@ -15,6 +15,7 @@ module.exports = {
'import/no-unresolved': 'off',
'import/extensions': 'off',
'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
'vue/html-self-closing': ['error', {
html: {
void: 'never',

View file

@ -55,3 +55,13 @@ in a separate `.add('function of component', ...`
### Storybook Build
After each client build, storybook build is also triggered and will be available in `dist/storybook`
### Vue Structure
Currently pages and components are mixed in `/src/components` this is not a good way to find the files easy.
Thats why each changed / upcoming page / component should be put in either `/src/components` or in the `/src/pages` directory.
Inside Pages, each page can have a subfolder which contains sub-components only needed for that page - otherwise it has to be added to the normal components folder.
At the end of all the changes - the components should only contain components needed between all pages

View file

@ -14679,32 +14679,57 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
"assert": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz",
"integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
"integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==",
"requires": {
"object-assign": "^4.1.1",
"util": "0.10.3"
"object.assign": "^4.1.4",
"util": "^0.10.4"
},
"dependencies": {
"define-properties": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"requires": {
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
},
"has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="
},
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
},
"object.assign": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
"integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.4",
"has-symbols": "^1.0.3",
"object-keys": "^1.1.1"
}
},
"util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"requires": {
"inherits": "2.0.1"
"inherits": "2.0.3"
}
}
}
@ -15543,9 +15568,9 @@
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
},
"bn.js": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz",
"integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ=="
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
},
"bonjour": {
"version": "3.5.0",
@ -15762,7 +15787,7 @@
"brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
},
"browser-process-hrtime": {
"version": "1.0.0",
@ -15834,9 +15859,9 @@
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@ -15915,12 +15940,12 @@
"buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
"integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="
},
"builtin-status-codes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
"integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="
},
"cacache": {
"version": "12.0.3",
@ -16793,7 +16818,7 @@
"constants-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
"integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
"integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="
},
"contains-path": {
"version": "0.1.0",
@ -16907,9 +16932,9 @@
}
},
"core-js": {
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.1.tgz",
"integrity": "sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ=="
"version": "3.32.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz",
"integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ=="
},
"core-js-compat": {
"version": "3.11.0",
@ -17037,9 +17062,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
@ -17376,7 +17401,7 @@
"de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0="
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
},
"debug": {
"version": "4.1.1",
@ -17670,9 +17695,9 @@
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
"integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"requires": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
@ -17819,9 +17844,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
@ -18598,13 +18623,29 @@
}
},
"eslint-plugin-vue": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz",
"integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.20.0.tgz",
"integrity": "sha512-oVNDqzBC9h3GO+NTgWeLMhhGigy6/bQaQbHS+0z7C4YEu/qK/yxHvca/2PTZtGNPsCrHwOTgKMrwu02A9iPBmw==",
"requires": {
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0",
"semver": "^5.6.0",
"vue-eslint-parser": "^7.0.0"
"semver": "^6.3.0",
"vue-eslint-parser": "^7.10.0"
},
"dependencies": {
"eslint-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"eslint-scope": {
@ -20578,9 +20619,9 @@
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@ -20724,7 +20765,7 @@
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"requires": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
@ -21030,7 +21071,7 @@
"https-browserify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
"integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
"integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="
},
"https-proxy-agent": {
"version": "5.0.1",
@ -21886,7 +21927,7 @@
"is-window": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
"integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0="
"integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg=="
},
"is-windows": {
"version": "1.0.2",
@ -23254,9 +23295,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
@ -23323,7 +23364,7 @@
"minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
},
"minimatch": {
"version": "3.0.4",
@ -24388,7 +24429,7 @@
"punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
}
}
},
@ -24787,7 +24828,7 @@
"os-browserify": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
"integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc="
"integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
},
"os-homedir": {
"version": "1.0.2",
@ -25089,9 +25130,9 @@
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
},
"pbkdf2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
"integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
"integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
"requires": {
"create-hash": "^1.1.2",
"create-hmac": "^1.1.4",
@ -26147,9 +26188,9 @@
},
"dependencies": {
"bn.js": {
"version": "4.11.9",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw=="
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
@ -26350,7 +26391,7 @@
"querystring-es3": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
"integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="
},
"querystringify": {
"version": "2.2.0",
@ -29503,7 +29544,7 @@
"to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
"integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="
"integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA=="
},
"to-fast-properties": {
"version": "2.0.0",
@ -29779,7 +29820,7 @@
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY="
"integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw=="
},
"tunnel-agent": {
"version": "0.6.0",
@ -30249,7 +30290,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw=="
}
}
},
@ -30285,7 +30326,7 @@
"uuid-browser": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz",
"integrity": "sha1-DwWkCu90+eWVHiDvv0SxGHHlZBA="
"integrity": "sha512-dsNgbLaTrd6l3MMxTtouOCFw4CBFc/3a+GgYA2YyrJvyQ1u6q4pcu3ktLoUZ/VN/Aw9WsauazbgsgdfVWgAKQg=="
},
"v8-compile-cache": {
"version": "2.1.0",
@ -30579,29 +30620,90 @@
}
},
"vue-eslint-parser": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz",
"integrity": "sha512-yR0dLxsTT7JfD2YQo9BhnQ6bUTLsZouuzt9SKRP7XNaZJV459gvlsJo4vT2nhZ/2dH9j3c53bIx9dnqU2prM9g==",
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz",
"integrity": "sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==",
"requires": {
"debug": "^4.1.1",
"eslint-scope": "^5.0.0",
"eslint-scope": "^5.1.1",
"eslint-visitor-keys": "^1.1.0",
"espree": "^6.1.2",
"esquery": "^1.0.1",
"lodash": "^4.17.15"
"espree": "^6.2.1",
"esquery": "^1.4.0",
"lodash": "^4.17.21",
"semver": "^6.3.0"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"requires": {
"esrecurse": "^4.1.0",
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
}
},
"espree": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
"integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
"requires": {
"acorn": "^7.1.1",
"acorn-jsx": "^5.2.0",
"eslint-visitor-keys": "^1.1.0"
}
},
"esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"requires": {
"estraverse": "^5.1.0"
},
"dependencies": {
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
}
}
},
"esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"requires": {
"estraverse": "^5.2.0"
},
"dependencies": {
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="
}
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vue-fragment": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A=="
},
"vue-functional-data-merge": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
@ -30666,6 +30768,340 @@
}
}
},
"vue-template-babel-compiler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-template-babel-compiler/-/vue-template-babel-compiler-2.0.0.tgz",
"integrity": "sha512-O0GOktQ5TZCZ5sWVl8CbyLBFriwwai7xDBtpdUI1xZSbbVVNf5Um/mDHYJXaHX6vfhmeAuohggXxIi0RPgXZ4g==",
"requires": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
"@babel/plugin-proposal-object-rest-spread": "^7.15.6",
"@babel/plugin-proposal-optional-chaining": "^7.14.2",
"@babel/plugin-transform-arrow-functions": "^7.14.5",
"@babel/plugin-transform-block-scoping": "^7.14.5",
"@babel/plugin-transform-computed-properties": "^7.14.5",
"@babel/plugin-transform-destructuring": "^7.14.5",
"@babel/plugin-transform-parameters": "^7.14.5",
"@babel/plugin-transform-spread": "^7.14.5",
"@babel/types": "^7.14.5",
"deepmerge": "^4.2.2"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
"integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
"requires": {
"@babel/highlight": "^7.18.6"
}
},
"@babel/compat-data": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz",
"integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ=="
},
"@babel/core": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz",
"integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==",
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.2",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-module-transforms": "^7.20.2",
"@babel/helpers": "^7.20.1",
"@babel/parser": "^7.20.2",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.1",
"semver": "^6.3.0"
}
},
"@babel/generator": {
"version": "7.20.4",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz",
"integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==",
"requires": {
"@babel/types": "^7.20.2",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
}
},
"@babel/helper-compilation-targets": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz",
"integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==",
"requires": {
"@babel/compat-data": "^7.20.0",
"@babel/helper-validator-option": "^7.18.6",
"browserslist": "^4.21.3",
"semver": "^6.3.0"
}
},
"@babel/helper-environment-visitor": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz",
"integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg=="
},
"@babel/helper-function-name": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz",
"integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==",
"requires": {
"@babel/template": "^7.18.10",
"@babel/types": "^7.19.0"
}
},
"@babel/helper-hoist-variables": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
"integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-module-imports": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-module-transforms": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz",
"integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==",
"requires": {
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-module-imports": "^7.18.6",
"@babel/helper-simple-access": "^7.20.2",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.2"
}
},
"@babel/helper-plugin-utils": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz",
"integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ=="
},
"@babel/helper-simple-access": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz",
"integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==",
"requires": {
"@babel/types": "^7.20.2"
}
},
"@babel/helper-skip-transparent-expression-wrappers": {
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz",
"integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==",
"requires": {
"@babel/types": "^7.20.0"
}
},
"@babel/helper-split-export-declaration": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
"integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
"requires": {
"@babel/types": "^7.18.6"
}
},
"@babel/helper-validator-identifier": {
"version": "7.19.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz",
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
},
"@babel/helper-validator-option": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
"integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw=="
},
"@babel/helpers": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz",
"integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==",
"requires": {
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.20.1",
"@babel/types": "^7.20.0"
}
},
"@babel/highlight": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
"integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
"requires": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz",
"integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg=="
},
"@babel/plugin-proposal-nullish-coalescing-operator": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz",
"integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.6",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3"
}
},
"@babel/plugin-proposal-object-rest-spread": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz",
"integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==",
"requires": {
"@babel/compat-data": "^7.20.1",
"@babel/helper-compilation-targets": "^7.20.0",
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/plugin-syntax-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-parameters": "^7.20.1"
}
},
"@babel/plugin-transform-arrow-functions": {
"version": "7.18.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz",
"integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.6"
}
},
"@babel/plugin-transform-block-scoping": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz",
"integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-computed-properties": {
"version": "7.18.9",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz",
"integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==",
"requires": {
"@babel/helper-plugin-utils": "^7.18.9"
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz",
"integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-parameters": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz",
"integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2"
}
},
"@babel/plugin-transform-spread": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz",
"integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==",
"requires": {
"@babel/helper-plugin-utils": "^7.19.0",
"@babel/helper-skip-transparent-expression-wrappers": "^7.18.9"
}
},
"@babel/template": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz",
"integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.18.10",
"@babel/types": "^7.18.10"
}
},
"@babel/traverse": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz",
"integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==",
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.20.1",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.19.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.20.1",
"@babel/types": "^7.20.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.20.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz",
"integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
"to-fast-properties": "^2.0.0"
}
},
"browserslist": {
"version": "4.21.4",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz",
"integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==",
"requires": {
"caniuse-lite": "^1.0.30001400",
"electron-to-chromium": "^1.4.251",
"node-releases": "^2.0.6",
"update-browserslist-db": "^1.0.9"
}
},
"caniuse-lite": {
"version": "1.0.30001434",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz",
"integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA=="
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg=="
},
"electron-to-chromium": {
"version": "1.4.284",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz",
"integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA=="
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA=="
},
"node-releases": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg=="
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vue-template-compiler": {
"version": "2.7.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz",
@ -30728,9 +31164,9 @@
},
"dependencies": {
"anymatch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"optional": true,
"requires": {
"normalize-path": "^3.0.0",
@ -30753,19 +31189,19 @@
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"optional": true,
"requires": {
"anymatch": "~3.1.1",
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
"readdirp": "~3.6.0"
}
},
"fill-range": {
@ -30778,15 +31214,15 @@
}
},
"fsevents": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"optional": true
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"optional": true,
"requires": {
"is-glob": "^4.0.1"
@ -30808,9 +31244,9 @@
"optional": true
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"optional": true,
"requires": {
"picomatch": "^2.2.1"
@ -30863,9 +31299,9 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
},
"webpack": {
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz",
"integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==",
"version": "4.47.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz",
"integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==",
"requires": {
"@webassemblyjs/ast": "1.9.0",
"@webassemblyjs/helper-module-context": "1.9.0",
@ -30890,37 +31326,6 @@
"terser-webpack-plugin": "^1.4.3",
"watchpack": "^1.7.4",
"webpack-sources": "^1.4.1"
},
"dependencies": {
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"requires": {
"randombytes": "^2.1.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"terser-webpack-plugin": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz",
"integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==",
"requires": {
"cacache": "^12.0.2",
"find-cache-dir": "^2.1.0",
"is-wsl": "^1.1.0",
"schema-utils": "^1.0.0",
"serialize-javascript": "^4.0.0",
"source-map": "^0.6.1",
"terser": "^4.1.2",
"webpack-sources": "^1.4.0",
"worker-farm": "^1.7.0"
}
}
}
},
"webpack-bundle-analyzer": {

View file

@ -32,12 +32,12 @@
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.32.1",
"core-js": "^3.32.2",
"dompurify": "^3.0.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
@ -58,12 +58,14 @@
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vue-template-compiler": "^2.7.10",
"vue-template-babel-compiler": "^2.0.0",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^4.46.0"
"webpack": "^4.47.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"

View file

@ -13,7 +13,7 @@
&:hover, &:focus {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&:disabled, &.disabled, &.btn-flat {
&.btn-flat {
box-shadow: none;
}
}
@ -264,6 +264,10 @@
box-shadow: none;
}
.btn-cancel {
color: $blue-10;
}
.btn-small {
font-size: 12px;
line-height: 1.33;

View file

@ -7,6 +7,10 @@
.dropdown-toggle:hover {
--caret-color: #{$purple-300};
&.disabled {
pointer-events: none;
}
}
.dropdown.show > .dropdown-toggle:not(.btn-success) {
@ -136,6 +140,8 @@
.dropdown-menu.show {
min-width: 100% !important;
overflow: scroll;
max-height: 400px;
}
}

View file

@ -26,11 +26,11 @@ input, textarea, input.form-control, textarea.form-control {
color: $gray-50;
border: 1px solid $gray-400;
&:hover:not(:disabled) {
&:hover:not(:disabled):not(:read-only) {
border-color: $gray-300;
}
&:active:not(:disabled), &:focus:not(:disabled) {
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only) {
border-color: $purple-400;
outline: 0;
box-shadow: none;
@ -56,13 +56,13 @@ input, textarea, input.form-control, textarea.form-control {
&.input-valid, &.input-invalid {
background-repeat: no-repeat;
background-position: center right 16px;
background-position: center right 0.5rem;
}
&.input-valid {
padding-right: 37px;
padding-right: 27px;
background-image: url(~@/assets/svg/for-css/check.svg);
background-size: 13px 10px;
background-size: 1rem;
}
&.input-invalid {
@ -91,8 +91,10 @@ input, textarea, input.form-control, textarea.form-control {
border-color: $gray-300;
}
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
&:not(:read-only) {
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
}
}
.input-group-prepend , .input-group-append {
@ -163,8 +165,22 @@ input, textarea, input.form-control, textarea.form-control {
input {
height: 30px;
border: 0;
background: $white !important;
}
&.is-valid {
border-color: $green-10 !important;
}
&.is-invalid {
border-color: $red-100 !important;
}
}
.input-error {
font-size: 12px;
line-height: 1.33;
color: $maroon-10;
}
.input-group-spaced {
@ -231,20 +247,20 @@ $bg-disabled-control: $gray-10;
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
border-color: 2 px solid $purple-400;
background-color: $purple-400;
}
&:focus:disabled~.custom-control-label::before,
&:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
}
@ -398,8 +414,6 @@ $bg-color: $purple-400;
margin-top: 0 !important;
}
// Disable default style Firefox for invalid elements.
// Selectors taken from view-source:resource://gre-resources/forms.css on Firefox
:not(output):-moz-ui-invalid {

View file

@ -1,55 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000"
viewBox="0 0 1000 1187.198"
version="1.1"
height="1187.198"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="Apple_1998.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="705"
id="namedview6"
showgrid="false"
inkscape:zoom="0.1767767"
inkscape:cx="-1066.5045"
inkscape:cy="964.94669"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
id="path4"
inkscape:connector-curvature="0" />
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.841 2.564c-.567.672-1.474 1.202-2.382 1.126-.113-.908.331-1.873.851-2.47C7.877.53 8.87.039 9.673 0c.095.946-.274 1.873-.832 2.564zm.823 1.306c-1.314-.076-2.439.747-3.063.747-.633 0-1.588-.71-2.627-.69-1.352.018-2.609.785-3.299 2.005-1.418 2.441-.369 6.055 1.002 8.042.67.984 1.474 2.063 2.533 2.025 1.002-.038 1.399-.653 2.609-.653 1.219 0 1.569.653 2.627.634 1.097-.019 1.787-.984 2.458-1.968.765-1.116 1.077-2.204 1.096-2.261-.019-.019-2.117-.823-2.136-3.245-.019-2.025 1.654-2.99 1.73-3.047-.946-1.4-2.42-1.551-2.93-1.59z" fill="#1A181D" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 670 B

View file

@ -1,3 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill="#24CC8F" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="vm46q29nca" d="M6.662 12.832c-.312 0-.61-.123-.831-.344L2 8.657l1.662-1.662 2.934 2.934L12.534 3l1.785 1.529-6.764 7.893c-.214.248-.521.396-.848.409l-.045.001"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g transform="translate(-306 -8) translate(306 8)">
<mask id="c8uzbxs4ob" fill="#fff">
<use xlink:href="#vm46q29nca"/>
</mask>
<use fill="#878190" xlink:href="#vm46q29nca"/>
<g fill="#20B780" mask="url(#c8uzbxs4ob)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 808 B

View file

@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="17"><defs><path id="a" d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z"/></defs><g transform="rotate(-90 8 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#BDA8FF" xlink:href="#a"/><g fill="#878190" mask="url(#b)"><path d="M0 0h16v16H0z"/></g></g></svg>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" >
<path d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z" id="myc95n2o6a"/>
</svg>

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 183 B

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd" opacity=".75" transform="translate(3 2)">
<path fill="#878190" d="M4 9h2V7H4v2zm4 1H2V6h6v4zM5 2c1.103 0 2 .897 2 2H3c0-1.103.897-2 2-2zm4 2.277V4c0-2.209-1.791-4-4-4S1 1.791 1 4v.277C.405 4.624 0 5.262 0 6v4c0 1.105.895 2 2 2h6c1.105 0 2-.895 2-2V6c0-.738-.405-1.376-1-1.723z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View file

@ -731,6 +731,8 @@ export default {
},
},
mounted () {
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});

View file

@ -245,12 +245,13 @@ import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
import copyIcon from '@/assets/svg/copy.svg';
import copyToClipboard from '@/mixins/copyToClipboard';
export default {
components: {
closeX,
},
mixins: [notifications],
mixins: [notifications, copyToClipboard],
data () {
return {
icons: Object.freeze({
@ -287,17 +288,10 @@ export default {
this.$root.$emit('bv::hide::modal', 'create-party-modal');
},
copyUsername () {
if (navigator.clipboard) {
navigator.clipboard.writeText(this.user.auth.local.username);
} else {
const copyText = document.createElement('textarea');
copyText.value = this.user.auth.local.username;
document.body.appendChild(copyText);
copyText.select();
document.execCommand('copy');
document.body.removeChild(copyText);
}
this.text(this.$t('usernameCopied'));
this.mixinCopyToClipboard(
this.user.auth.local.username,
this.$t('usernameCopied'),
);
},
seekParty () {
this.$store.dispatch('user:set', {

View file

@ -86,11 +86,6 @@
color: $gray-50;
}
.input-error {
color: $red-50;
font-size: 90%;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;

View file

@ -117,14 +117,14 @@ import * as quests from '@/../../common/script/content/quests';
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
import notificationsIcon from '@/assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import MessageCount from './messageCount.functional.vue';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
import successImage from '@/assets/svg/success.svg';
import starBadge from '@/assets/svg/star-badge.svg';
// Notifications
import CARD_RECEIVED from './notifications/cardReceived';
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
import CHALLENGE_INVITATION from './notifications/challengeInvitation.functional.vue';
import GIFT_ONE_GET_ONE from './notifications/g1g1';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';

View file

@ -56,7 +56,7 @@
>{{ $t('achievements') }}</a>
<router-link
class="topbar-dropdown-item dropdown-item"
:to="{name: 'site'}"
:to="{name: 'general'}"
>
{{ $t('settings') }}
</router-link>
@ -141,7 +141,7 @@
import { mapState } from '@/libs/store';
import userIcon from '@/assets/svg/user.svg';
import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount';
import MessageCount from './messageCount.functional.vue';
import { EVENTS } from '@/libs/events';
import { PAGES } from '@/libs/consts';

View file

@ -231,7 +231,7 @@
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="'Pet_Egg_'+currentDraggingEgg.key"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div class="popover-content">
@ -248,7 +248,7 @@
<div v-if="currentDraggingEgg != null">
<div
class="potion-icon"
:class="'Pet_Egg_'+currentDraggingEgg.key"
:class="`Pet_Egg_${currentDraggingEgg.key}`"
></div>
<div class="popover">
<div
@ -266,7 +266,7 @@
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div
@ -285,7 +285,7 @@
<div v-if="currentDraggingPotion != null">
<div
class="potion-icon"
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
></div>
<div class="popover">
<div

View file

@ -16,7 +16,7 @@
<span
v-drag.food="item.key"
class="item-content"
:class="'Pet_Food_'+item.key"
:class="`Pet_Food_${item.key}`"
@itemDragEnd="dragend($event)"
@itemDragStart="dragstart($event)"
></span>

View file

@ -6,10 +6,10 @@
>
<div class="potionEggGroup">
<div class="potionEggBackground">
<div :class="'Pet_HatchingPotion_'+hatchablePet.potionKey"></div>
<div :class="`Pet_HatchingPotion_${hatchablePet.potionKey}`"></div>
</div>
<div class="potionEggBackground">
<div :class="'Pet_Egg_'+hatchablePet.eggKey"></div>
<div :class="`Pet_Egg_${hatchablePet.eggKey}`"></div>
</div>
</div>
<h4 class="title">

View file

@ -268,7 +268,7 @@
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="'Pet_Food_'+currentDraggingFood.key"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div
@ -287,7 +287,7 @@
<div v-if="currentDraggingFood != null">
<div
class="food-icon"
:class="'Pet_Food_'+currentDraggingFood.key"
:class="`Pet_Food_${currentDraggingFood.key}`"
></div>
<div class="popover">
<div

View file

@ -22,18 +22,19 @@
v-if="currentEvent && currentEvent.promo === 'g1g1'"
class="g1g1-margin d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
v-html="icons.bigGift"
></div>
<div
v-once
class="svg-big-gift"
v-html="icons.bigGift"
></div>
</div>
<div
v-else
class="d-flex flex-column align-items-center">
v-else
class="d-flex flex-column align-items-center"
>
<div
class="svg-big-gift"
v-once
class="svg-big-gift"
v-html="icons.bigGift"
></div>
</div>
@ -49,9 +50,10 @@
></div>
</div>
<div
v-else
class="modal-close"
@click="close()">
v-else
class="modal-close"
@click="close()"
>
<div
class="svg-icon"
v-html="icons.close"
@ -65,26 +67,15 @@
name="selectUser"
novalidate="novalidate"
>
<div class="input-group">
<input
id="selectUser"
v-model="userSearchTerm"
class="form-control"
type="text"
ref="textBox"
:placeholder="$t('usernameOrUserId')"
:class="{
'input-valid': foundUser._id,
'is-invalid input-invalid': userNotFound,
}"
>
</div>
<div
v-if="userSearchTerm.length > 0 && userNotFound"
class="input-error text-center mt-2"
>
{{ $t('userWithUsernameOrUserIdNotFound') }}
</div>
<validated-text-input
id="selectUser"
v-model="userSearchTerm"
:is-valid="foundUser._id"
:placeholder="$t('usernameOrUserId')"
:invalid-issues="userInputInvalidIssues"
/>
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
<button
class="btn btn-primary mx-auto mt-2"
@ -104,16 +95,12 @@
</div>
</button>
<div
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
v-html="$t('cancel')"
@click="close()"
v-if="currentEvent && currentEvent.promo ==='g1g1'"
class="g1g1-cancel d-flex justify-content-center"
@click="close()"
v-html="$t('cancel')"
>
{{ $t('cancel') }}
</div>
<div
v-else>
</div>
</div>
</div>
</div>
</div>
@ -121,182 +108,179 @@
slot="modal-footer"
class="g1g1-fine-print text-center pt-3"
>
<strong>
{{ $t ('howItWorks') }}
<strong v-once>
{{ $t('howItWorks') }}
</strong>
<p
v-once
class="mx-5 mt-1"
>
{{ $t ('g1g1HowItWorks') }}
{{ $t('g1g1HowItWorks') }}
</p>
<strong>
{{ $t ('limitations') }}
<strong v-once>
{{ $t('limitations') }}
</strong>
<p
v-once
class="mx-5 mt-1"
>
{{ $t ('g1g1Limitations') }}
{{ $t('g1g1Limitations') }}
</p>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/mixins.scss';
@import '~@/assets/scss/mixins.scss';
#select-user-modal {
.modal-content {
width:448px;
#select-user-modal {
.modal-content {
width: 448px;
}
.input-group {
margin-top: 0rem;
}
.modal-dialog {
width: 448px;
}
.modal-footer {
padding: 0rem;
> * {
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
.input-group {
margin-top: 0rem;
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
.modal-dialog {
width: 448px;
}
.modal-footer {
padding: 0rem;
> * {
margin: 0rem 0.25rem 0.25rem 0.25rem;
}
}
body.modal-open .modal {
display: flex !important;
height: 100%;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
body.modal-open .modal .modal-dialog {
margin: auto;
}
}
</style>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
a:not([href]) {
font-size: 0.875rem;
line-height: 1.71;
}
a:not([href]) {
#selectUser {
width: 22rem;
border: 0px;
color: $gray-50;
}
font-size: 0.875rem;
line-height: 1.71;
}
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
#selectUser {
width: 22rem;
border: 0px;
color: $gray-50;
}
.g1g1 {
background-image: url('~@/assets/images/g1g1-send.png');
background-size: 446px 152px;
width: 446px;
height: 152px;
margin: -16px 0px 0px -16px;
border-radius: 4.8px 4.8px 0px 0px;
padding: 24px;
color: $white;
h1 {
font-size: 1.25rem;
line-height: 1.4;
color: $white;
h1 {
font-size: 1.25rem;
line-height: 1.4;
color: $white;
}
p {
font-size: 0.75rem;
line-height: 1.33;
margin-left: 4rem;
margin-right: 4rem;
margin-bottom: 0rem;
}
}
.g1g1-margin {
margin-top: 24px;
}
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
p {
font-size: 0.75rem;
line-height: 1.33;
margin-left: 4rem;
margin-right: 4rem;
margin-bottom: 0rem;
}
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-margin {
margin-top: 24px;
}
.g1g1-svg-icon {
width: 12px;
height: 12px;
.g1g1-cancel {
margin-top: 16px;
color: $blue-10;
cursor: pointer;
}
& ::v-deep svg path {
fill: #FFFFFF;
}
.g1g1-fine-print {
color: $gray-100;
background-color: $gray-700;
font-size: 0.75rem;
line-height: 1.33;
}
.g1g1-modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.g1g1-svg-icon {
width: 12px;
height: 12px;
& ::v-deep svg path {
fill: #FFFFFF;
}
}
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;
margin-top: 0.5rem;
}
.input-group:focus-within {
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.svg-icon {
width: 12px;
height: 12px;
}
.g1g1-modal-dialog {
margin-top: 10vh;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;
margin-top: 0.5rem;
}
.input-group:focus-within {
border-color: $purple-500;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
color: $purple-300;
padding-top: 1rem;
}
.svg-big-gift {
width: 176px;
height: 64px;
}
.modal-close {
position: absolute;
width: 18px;
height: 18px;
padding: 4px;
right: 16px;
top: 16px;
cursor: pointer;
.svg-icon {
width: 12px;
height: 12px;
}
}
</style>
@ -308,8 +292,10 @@ import isUUID from 'validator/lib/isUUID';
import { mapState } from '@/libs/store';
import closeIcon from '@/assets/svg/close.svg';
import bigGiftIcon from '@/assets/svg/big-gift.svg';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
export default {
components: { ValidatedTextInput },
data () {
return {
userNotFound: false,
@ -332,6 +318,12 @@ export default {
if (this.userSearchTerm.length < 1) return true;
return typeof this.foundUser._id === 'undefined';
},
userInputInvalidIssues () {
return this.userSearchTerm.length > 0 && this.userNotFound
? [this.$t('userWithUsernameOrUserIdNotFound')]
: [''];
},
},
watch: {
userSearchTerm: {

View file

@ -1,190 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-6">
<h2>{{ $t('API') }}</h2>
<p>{{ $t('APIText') }}</p>
<div class="section">
<h6>{{ $t('userId') }}</h6>
<pre class="prettyprint">{{ user.id }}</pre>
<h6>{{ $t('APIToken') }}</h6>
<div class="d-flex align-items-center mb-3">
<button
class="btn btn-secondary"
@click="showApiToken = !showApiToken"
>
{{ $t(`${showApiToken ? 'hide' : 'show'}APIToken`) }}
</button>
<pre
v-if="showApiToken"
class="prettyprint ml-4 mb-0"
>{{ apiToken }}</pre>
</div>
<p v-html="$t('APITokenWarning', { hrefTechAssistanceEmail })"></p>
</div>
<div class="section">
<h3>{{ $t('thirdPartyApps') }}</h3>
<p v-html="$t('thirdPartyTools')"></p>
<hr>
</div>
</div>
<div class="col-6">
<h2>{{ $t('webhooks') }}</h2>
<p v-html="$t('webhooksInfo')"></p>
<table class="table table-striped">
<thead v-if="user.webhooks.length">
<tr>
<th>{{ $t('enabled') }}</th>
<th>{{ $t('webhookURL') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr
v-for="(webhook, index) in user.webhooks"
:key="webhook.id"
>
<td>
<input
v-model="webhook.enabled"
type="checkbox"
@change="saveWebhook(webhook, index)"
>
</td>
<td>
<input
v-model="webhook.url"
class="form-control"
type="url"
>
</td>
<td>
<div
class="btn btn-danger checklist-icons mr-2"
@click="deleteWebhook(webhook, index)"
>
<span
class="glyphicon glyphicon-trash"
:tooltip="$t('delete')"
> {{ $t('delete') }} </span>
</div>
<div
class="btn btn-primary checklist-icons"
@click="saveWebhook(webhook, index)"
>
{{ $t('subUpdateTitle') }}
</div>
</td>
</tr>
<tr>
<td colspan="2">
<div class="form-horizontal">
<div class="form-group col-sm-10">
<input
v-model="newWebhook.url"
class="form-control"
type="url"
:placeholder="$t('webhookURL')"
>
</div>
<div class="col-sm-2">
<button
class="btn btn-sm btn-primary"
type="submit"
@click="addWebhook(newWebhook.url)"
>
{{ $t('add') }}
</button>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style scoped>
.section {
margin-top: 2em;
}
li span
{
display: block;
}
</style>
<script>
import { mapState } from '@/libs/store';
import uuid from '@/../../common/script/libs/uuid';
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
export default {
data () {
return {
newWebhook: {
url: '',
},
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
showApiToken: false,
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
apiToken () {
return this.credentials.API_TOKEN;
},
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('API'),
});
window.addEventListener('message', this.receiveMessage, false);
},
destroy () {
window.removeEventListener('message', this.receiveMessage);
},
methods: {
receiveMessage (eventFrom) {
if (eventFrom.origin !== 'https://www.spritely.app') return;
const creds = {
userId: this.user._id,
apiToken: this.credentials.API_TOKEN,
};
eventFrom.source.postMessage(creds, eventFrom.origin);
},
async addWebhook (url) {
const webhookInfo = {
id: uuid(),
type: 'taskActivity',
options: {
created: false,
updated: false,
deleted: false,
scored: true,
},
url,
enabled: true,
};
const webhook = await this.$store.dispatch('user:addWebhook', { webhookInfo });
this.user.webhooks.push(webhook);
this.newWebhook.url = '';
},
async saveWebhook (webhook, index) {
delete webhook._editing;
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
},
async deleteWebhook (webhook, index) {
delete webhook._editing;
await this.$store.dispatch('user:deleteWebhook', { webhook });
this.user.webhooks.splice(index, 1);
},
},
};
</script>

View file

@ -1,26 +0,0 @@
<template>
<div class="row">
<div class="col-md-6">
<h2>{{ $t('dataExport') }}</h2>
<small>{{ $t('saveData') }}</small>
<h4>{{ $t('habitHistory') }}</h4>
{{ $t('exportHistory') }}
<a href="/export/history.csv">{{ $t('csv') }}</a>
<h4>{{ $t('userData') }}</h4>
{{ $t('exportUserData') }}
<a href="/export/userdata.xml">{{ $t('xml') }}</a>
<a href="/export/userdata.json">{{ $t('json') }}</a>
</div>
</div>
</template>
<script>
export default {
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('dataExport'),
});
},
};
</script>

View file

@ -1,132 +0,0 @@
<template>
<div>
<div>
<h5>{{ $t('dayStartAdjustment') }}</h5>
<div class="mb-4">
{{ $t('customDayStartInfo1') }}
</div>
<h3 v-once>{{ $t('adjustment') }}</h3>
<div class="form-horizontal">
<div class="form-group">
<div class="">
<select
v-model="newDayStart"
class="form-control"
>
<option
v-for="option in dayStartOptions"
:key="option.value"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</div>
<div>
<button
class="btn btn-primary full-width mt-3"
:disabled="newDayStart === user.preferences.dayStart"
@click="openDayStartModal()"
>
{{ $t('save') }}
</button>
</div>
</div>
</div>
</div>
<div class="form-horizontal">
<div class="form-group">
<small>
<p v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})"></p>
<p v-html="$t('timezoneInfo')"></p>
</small>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import moment from 'moment';
import getUtcOffset from '../../../../common/script/fns/getUtcOffset';
import { mapState } from '@/libs/store';
export default {
name: 'dayStartAdjustment',
data () {
const dayStartOptions = [];
for (let number = 0; number <= 12; number += 1) {
const meridian = number < 12 ? 'AM' : 'PM';
const hour = number % 12;
const timeWithMeridian = `(${hour || 12}:00 ${meridian})`;
const option = {
value: number,
name: `+${number} hours ${timeWithMeridian}`,
};
if (number === 0) {
option.name = `Default ${timeWithMeridian}`;
}
dayStartOptions.push(option);
}
return {
newDayStart: 0,
dayStartOptions,
};
},
mounted () {
this.newDayStart = this.user.preferences.dayStart;
},
computed: {
...mapState({
user: 'user.data',
}),
timezoneOffsetToUtc () {
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
return `UTC${offsetString}`;
},
dayStart () {
return this.user.preferences.dayStart;
},
},
methods: {
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v4/user/custom-day-start', {
dayStart: this.newDayStart,
});
// @TODO
// Notification.text(response.data.data.message);
},
openDayStartModal () {
const nextCron = this.calculateNextCron();
// @TODO: Add generic modal
if (!window.confirm(this.$t('sureChangeCustomDayStartTime', { time: nextCron }))) return; // eslint-disable-line no-alert
this.saveDayStart();
// $rootScope.openModal('change-day-start', { scope: $scope });
},
calculateNextCron () {
let nextCron = moment()
.hours(this.newDayStart)
.minutes(0)
.seconds(0)
.milliseconds(0);
const currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
},
},
};
</script>
<style scoped>
.full-width {
width: 100%;
}
</style>

View file

@ -1,86 +0,0 @@
<template>
<b-modal
id="delete"
:title="$t('deleteAccount')"
:hide-footer="true"
size="md"
>
<div class="modal-body">
<br>
<strong v-if="user.auth.local.has_password">{{ $t('deleteLocalAccountText') }}</strong>
<strong
v-if="!user.auth.local.has_password"
>{{ $t('deleteSocialAccountText', {magicWord: 'DELETE'}) }}</strong>
<div class="row mt-3">
<div class="col-6">
<input
v-model="password"
class="form-control"
type="password"
>
</div>
</div>
<div class="row mt-3">
<div
id="feedback"
class="col-12 form-group"
>
<label for="feedbackTextArea">{{ $t('feedback') }}</label>
<textarea
id="feedbackTextArea"
v-model="feedback"
class="form-control"
></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('neverMind') }}
</button>
<button
class="btn btn-danger"
:disabled="!password"
@click="deleteAccount()"
>
{{ $t('deleteDo') }}
</button>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
export default {
data () {
return {
password: '',
feedback: '',
};
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'delete');
},
async deleteAccount () {
await axios.delete('/api/v4/user', {
data: {
password: this.password,
feedback: this.feedback,
},
});
localStorage.clear();
window.location.href = '/static/home';
this.$root.$emit('bv::hide::modal', 'delete');
},
},
};
</script>

View file

@ -1,158 +0,0 @@
<template>
<div class="row">
<secondary-menu class="col-12">
<router-link
class="nav-link"
:to="{name: 'site'}"
exact="exact"
:class="{'active': $route.name === 'site'}"
>
{{ $t('site') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'api'}"
:class="{'active': $route.name === 'api'}"
>
{{ $t('API') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'dataExport'}"
:class="{'active': $route.name === 'dataExport'}"
>
{{ $t('dataExport') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'promoCode'}"
:class="{'active': $route.name === 'promoCode'}"
>
{{ $t('promoCode') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'subscription'}"
:class="{'active': $route.name === 'subscription'}"
>
{{ $t('subscription') }}
</router-link>
<router-link
v-if="hasPermission(user, 'userSupport')"
class="nav-link"
:to="{name: 'transactions'}"
:class="{'active': $route.name === 'transactions'}"
>
{{ $t('transactions') }}
</router-link>
<router-link
class="nav-link"
:to="{name: 'notifications'}"
:class="{'active': $route.name === 'notifications'}"
>
{{ $t('notifications') }}
</router-link>
</secondary-menu>
<div
v-if="$route.name === 'subscription' && promo === 'g1g1'"
class="g1g1-banner d-flex justify-content-center"
@click="showSelectUser"
>
<div
v-once
class="svg-icon svg-gifts left-gift"
v-html="icons.gifts"
>
</div>
<div class="d-flex flex-column align-items-center text-center">
<strong
class="mt-auto mb-1"
> {{ $t('g1g1Event') }} </strong>
<p
class="mb-auto"
>
{{ $t('g1g1Details') }}
</p>
</div>
<div
v-once
class="svg-icon svg-gifts right-gift"
v-html="icons.gifts"
>
</div>
</div>
<div class="col-12">
<router-view />
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
strong {
font-size: 1rem;
line-height: 1.25;
}
.g1g1-banner {
color: $white;
width: 100%;
height: 5.75rem;
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
cursor: pointer;
}
.left-gift {
margin: auto 3rem auto auto;
}
.right-gift {
margin: auto auto auto 3rem;
filter: flipH;
transform: scaleX(-1);
}
.svg-gifts {
width: 3.5rem;
}
</style>
<script>
import find from 'lodash/find';
import { mapState } from '@/libs/store';
import SecondaryMenu from '@/components/secondaryMenu';
import gifts from '@/assets/svg/gifts-vertical.svg';
import { userStateMixin } from '../../mixins/userState';
export default {
components: {
SecondaryMenu,
},
mixins: [userStateMixin],
data () {
return {
icons: Object.freeze({
gifts,
}),
};
},
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
}),
currentEvent () {
return find(this.currentEventList, event => Boolean(event.promo));
},
promo () {
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
return this.currentEvent.promo;
},
},
methods: {
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');
},
},
};
</script>

View file

@ -1,143 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1>{{ $t('notifications') }}</h1>
</div>
<div class="col-12">
<div class="checkbox">
<label>
<input
v-model="user.preferences.pushNotifications.unsubscribeFromAll"
type="checkbox"
class="mr-2"
@change="set('pushNotifications', 'unsubscribeFromAll')"
>
<span>{{ $t('unsubscribeAllPush') }}</span>
</label>
</div>
<br>
<div class="checkbox">
<label>
<input
v-model="user.preferences.emailNotifications.unsubscribeFromAll"
type="checkbox"
class="mr-2"
@change="set('emailNotifications', 'unsubscribeFromAll')"
>
<span>{{ $t('unsubscribeAllEmails') }}</span>
</label>
</div>
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
</div>
<div class="col-8">
<table class="table">
<tr>
<td></td>
<th>
<span>{{ $t('email') }}</span>
</th>
<th>
<span>{{ $t('push') }}</span>
</th>
</tr>
<tr
v-for="notification in notificationsIds"
:key="notification"
>
<td>
<span>{{ $t(notification) }}</span>
</td>
<td>
<input
v-model="user.preferences.emailNotifications[notification]"
type="checkbox"
@change="set('emailNotifications', notification)"
>
</td>
<td v-if="onlyEmailsIds.indexOf(notification) === -1">
<input
v-model="user.preferences.pushNotifications[notification]"
type="checkbox"
@change="set('pushNotifications', notification)"
>
</td><td v-else>
&nbsp;
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
import { mapState } from '@/libs/store';
import notificationsMixin from '@/mixins/notifications';
export default {
mixins: [notificationsMixin],
data () {
return {
notificationsIds: Object.freeze([
'majorUpdates',
'onboarding',
'newPM',
'wonChallenge',
'giftedGems',
'giftedSubscription',
'invitedParty',
'invitedGuild',
'kickedGroup',
'questStarted',
'invitedQuest',
'importantAnnouncements',
'weeklyRecaps',
'subscriptionReminders',
]),
// list of email-only notifications
onlyEmailsIds: Object.freeze([
'kickedGroup',
'importantAnnouncements',
'weeklyRecaps',
'onboarding',
'subscriptionReminders',
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('notifications'),
});
// If ?unsubFrom param is passed with valid email type,
// automatically unsubscribe users from that email and
// show an alert
// A simple object to map the key stored in the db (user.preferences.emailNotification[key])
// to its string id but ONLY when the preferences' key and the string key don't match
const MAP_PREF_TO_EMAIL_STRING = {
importantAnnouncements: 'inactivityEmails',
};
const { unsubFrom } = this.$route.query;
if (unsubFrom) {
await this.$store.dispatch('user:set', {
[`preferences.emailNotifications.${unsubFrom}`]: false,
});
const emailTypeString = this.$t(MAP_PREF_TO_EMAIL_STRING[unsubFrom] || unsubFrom);
this.text(this.$t('correctlyUnsubscribedEmailType', { emailType: emailTypeString }));
}
},
methods: {
set (preferenceType, notification) {
const settings = {};
settings[`preferences.${preferenceType}.${notification}`] = this.user.preferences[preferenceType][notification];
this.$store.dispatch('user:set', settings);
},
},
};
</script>

View file

@ -1,117 +0,0 @@
<template>
<div class="row standard-page">
<div class="col-md-6">
<h2>{{ $t('promoCode') }}</h2>
<div
class="form-inline"
role="form"
>
<input
v-model="couponCode"
class="form-control"
type="text"
:placeholder="$t('promoPlaceholder')"
>
<button
class="btn btn-primary"
@click="enterCoupon()"
>
{{ $t('submit') }}
</button>
</div>
<div>
<small>{{ $t('couponText') }}</small>
</div>
<div v-if="user.permissions.coupons">
<hr>
<h4>{{ $t('generateCodes') }}</h4>
<div
class="form"
role="form"
>
<div class="form-group">
<input
v-model="codes.event"
class="form-control"
type="text"
placeholder="Event code (eg, 'wondercon')"
>
</div>
<div class="form-group">
<input
v-model="codes.count"
class="form-control"
type="number"
placeholder="Number of codes to generate (eg, 250)"
>
</div>
<div class="form-group">
<button
class="btn btn-primary"
type="submit"
@click="generateCodes(codes)"
>
{{ $t('generate') }}
</button>
<a
class="btn btn-secondary"
:href="getCodesUrl"
>{{ $t('getCodes') }}</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
export default {
mixins: [notifications],
data () {
return {
codes: {
event: '',
count: '',
},
couponCode: '',
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
getCodesUrl () {
if (!this.user) return '';
return '/api/v4/coupons';
},
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('promoCode'),
});
},
methods: {
generateCodes () {
// $http.post(ApiUrl.get() + '/api/v2/coupons/generate/
// '+codes.event+'?count='+(codes.count || 1))
// .success(function(res,code){
// $scope._codes = {};
// if (code!==200) return;
// window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+
// '&apiToken='+User.settings.auth.apiToken;
// })
},
async enterCoupon () {
const code = await axios.post(`/api/v4/coupons/enter/${this.couponCode}`);
if (!code) return;
this.$store.state.user.data = code.data.data;
this.text(this.$t('promoCodeApplied'));
},
},
};
</script>

View file

@ -1,46 +0,0 @@
<template>
<b-modal
id="reset"
:title="$t('resetAccount')"
:hide-footer="true"
size="md"
>
<p>{{ $t('resetText1') }}</p>
<p>{{ $t('resetText2') }}</p>
<div class="modal-footer">
<button
class="btn btn-primary"
@click="close()"
>
{{ $t('neverMind') }}
</button>
<button
class="btn btn-danger"
@click="reset()"
>
{{ $t('resetDo') }}
</button>
</div>
</b-modal>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
export default {
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'reset');
},
async reset () {
await axios.post('/api/v4/user/reset');
this.$router.push('/');
setTimeout(() => window.location.reload(true), 100);
},
},
};
</script>

View file

@ -1,210 +0,0 @@
<template>
<b-modal
id="restore"
:title="$t('fixValues')"
:hide-footer="true"
size="lg"
>
<p>{{ $t('fixValuesText1') }}</p>
<p>{{ $t('fixValuesText2') }}</p>
<div class="form-horizontal">
<h3>{{ $t('stats') }}</h3>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('health') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.hp"
class="form-control"
type="number"
step="any"
data-for="stats.hp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('experience') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.exp"
class="form-control"
type="number"
step="any"
data-for="stats.exp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('gold') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.gp"
class="form-control"
type="number"
step="any"
data-for="stats.gp"
>
</div>
<!--input.form-control(type='number',
step="any", data-for='stats.gp', v-model='restoreValues.stats.gp',disabled)-->
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('mana') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.mp"
class="form-control"
type="number"
step="any"
data-for="stats.mp"
>
</div>
</div>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('level') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.stats.lvl"
class="form-control"
type="number"
data-for="stats.lvl"
>
</div>
</div>
<h3>{{ $t('achievements') }}</h3>
<div class="form-group row">
<div class="col-sm-3">
<label class="control-label">{{ $t('fix21Streaks') }}</label>
</div>
<div class="col-sm-9">
<input
v-model="restoreValues.achievements.streak"
class="form-control"
type="number"
data-for="achievements.streak"
>
</div>
</div>
</div>
<div class="modal-footer">
<button
class="btn btn-danger"
@click="close()"
>
{{ $t('discardChanges') }}
</button>
<button
class="btn btn-primary"
@click="restore()"
>
{{ $t('saveAndClose') }}
</button>
</div>
</b-modal>
</template>
<script>
import clone from 'lodash/clone';
import { MAX_LEVEL_HARD_CAP } from '@/../../common/script/constants';
import { mapState } from '@/libs/store';
export default {
data () {
return {
restoreValues: {
stats: {
hp: 0,
mp: 0,
gp: 0,
exp: 0,
lvl: 0,
},
achievements: {
streak: 0,
},
},
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.restoreValues.stats = clone(this.user.stats);
this.restoreValues.achievements.streak = clone(this.user.achievements.streak);
},
methods: {
close () {
this.validateInputs();
this.$root.$emit('bv::hide::modal', 'restore');
},
restore () {
if (!this.validateInputs()) {
return;
}
if (this.restoreValues.stats.lvl > MAX_LEVEL_HARD_CAP) {
this.restoreValues.stats.lvl = MAX_LEVEL_HARD_CAP;
}
const userChangedLevel = this.restoreValues.stats.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.stats.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) this.restoreValues.stats.exp = 0;
this.user.stats = clone(this.restoreValues.stats);
this.user.achievements.streak = clone(this.restoreValues.achievements.streak);
const settings = {
'stats.hp': Number(this.restoreValues.stats.hp),
'stats.exp': Number(this.restoreValues.stats.exp),
'stats.gp': Number(this.restoreValues.stats.gp),
'stats.lvl': Number(this.restoreValues.stats.lvl),
'stats.mp': Number(this.restoreValues.stats.mp),
'achievements.streak': Number(this.restoreValues.achievements.streak),
};
this.$store.dispatch('user:set', settings);
this.$root.$emit('bv::hide::modal', 'restore');
},
validateInputs () {
const canRestore = ['hp', 'exp', 'gp', 'mp'];
let valid = true;
for (const stat of canRestore) {
if (this.restoreValues.stats[stat] === ''
|| this.restoreValues.stats[stat] < 0
) {
this.restoreValues.stats[stat] = this.user.stats[stat];
valid = false;
}
}
const inputLevel = Number(this.restoreValues.stats.lvl);
if (this.restoreValues.stats.lvl === ''
|| !Number.isInteger(inputLevel)
|| inputLevel < 1) {
this.restoreValues.stats.lvl = this.user.stats.lvl;
valid = false;
}
const inputStreak = Number(this.restoreValues.achievements.streak);
if (this.restoreValues.achievements.streak === ''
|| !Number.isInteger(inputStreak)
|| inputStreak < 0) {
this.restoreValues.achievements.streak = this.user.achievements.streak;
valid = false;
}
return valid;
},
},
};
</script>

View file

@ -1,859 +0,0 @@
<template>
<div class="row standard-page">
<restore-modal />
<reset-modal />
<delete-modal />
<h1 class="col-12">
{{ $t('settings') }}
</h1>
<div class="col-sm-6">
<div class="sleep">
<h5>{{ $t('pauseDailies') }}</h5>
<h4>{{ $t('sleepDescription') }}</h4>
<ul>
<li v-once>
{{ $t('sleepBullet1') }}
</li>
<li v-once>
{{ $t('sleepBullet2') }}
</li>
<li v-once>
{{ $t('sleepBullet3') }}
</li>
</ul>
<button
v-if="!user.preferences.sleep"
v-once
class="sleep btn btn-primary btn-block pause-button"
@click="toggleSleep()"
>
{{ $t('pauseDailies') }}
</button>
<button
v-if="user.preferences.sleep"
v-once
class="btn btn-secondary btn-block pause-button"
@click="toggleSleep()"
>
{{ $t('unpauseDailies') }}
</button>
</div>
<hr>
<div class="form-horizontal">
<h5>{{ $t('language') }}</h5>
<select
class="form-control"
:value="user.preferences.language"
@change="changeLanguage($event)"
>
<option
v-for="lang in availableLanguages"
:key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</select>
<small>
{{ $t('americanEnglishGovern') }}
<br>
<strong v-html="$t('helpWithTranslation')"></strong>
</small>
</div>
<hr>
<div class="form-horizontal">
<h5>{{ $t('dateFormat') }}</h5>
<select
v-model="user.preferences.dateFormat"
class="form-control"
@change="set('dateFormat')"
>
<option
v-for="dateFormat in availableFormats"
:key="dateFormat"
:value="dateFormat"
>
{{ dateFormat }}
</option>
</select>
</div>
<hr>
<div class="form-horizontal">
<div class="form-group">
<h5>{{ $t('audioTheme') }}</h5>
<select
v-model="user.preferences.sound"
class="form-control"
@change="changeAudioTheme"
>
<option
v-for="sound in availableAudioThemes"
:key="sound"
:value="sound"
>
{{ $t(`audioTheme_${sound}`) }}
</option>
</select>
</div>
<button
v-once
class="btn btn-primary btn-xs"
@click="playAudio"
>
{{ $t('demo') }}
</button>
</div>
<hr>
<div
v-if="hasClass"
class="form-horizontal"
>
<h5>{{ $t('characterBuild') }}</h5>
<h6 v-once>
{{ $t('class') + ': ' }}
<!-- @TODO: what is classText-->
<!-- span(v-if='classText') {{ classText }}&nbsp;-->
<button
v-once
class="btn btn-danger btn-xs"
@click="changeClassForUser(true)"
>
{{ $t('changeClass') }}
</button>
<small class="cost">
&nbsp; 3 {{ $t('gems') }}
<!-- @TODO add icon span.Pet_Currency_Gem1x.inline-gems-->
</small>
</h6>
<hr>
</div>
<div>
<div
class="checkbox"
id="preferenceAdvancedCollapsed"
>
<label>
<input
v-model="user.preferences.advancedCollapsed"
type="checkbox"
class="mr-2"
@change="set('advancedCollapsed')"
>
<span class="hint">
{{ $t('startAdvCollapsed') }}
</span>
<b-popover
target="preferenceAdvancedCollapsed"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('startAdvCollapsedPop')"
/>
</label>
</div>
<div
v-if="party.memberCount === 1"
class="checkbox"
id="preferenceDisplayInviteAtOneMember"
>
<label>
<input
v-model="user.preferences.displayInviteToPartyWhenPartyIs1"
type="checkbox"
class="mr-2"
@change="set('displayInviteToPartyWhenPartyIs1')"
>
<span class="hint">
{{ $t('displayInviteToPartyWhenPartyIs1') }}
</span>
</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.levelUp"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'levelUp')"
>
<label>{{ $t('suppressLevelUpModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.hatchPet"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'hatchPet')"
>
<label>{{ $t('suppressHatchPetModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.raisePet"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'raisePet')"
>
<label>{{ $t('suppressRaisePetModal') }}</label>
</div>
<div class="checkbox">
<input
v-model="user.preferences.suppressModals.streak"
type="checkbox"
class="mr-2"
@change="set('suppressModals', 'streak')"
>
<label>{{ $t('suppressStreakModal') }}</label>
</div>
<hr>
<button
id="buttonShowBailey"
class="btn btn-primary mr-2 mb-2"
@click="showBailey()"
>
{{ $t('showBailey') }}
<b-popover
target="buttonShowBailey"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('showBaileyPop')"
/>
</button>
<button
id="buttonFCV"
class="btn btn-primary mr-2 mb-2"
@click="openRestoreModal()"
>
{{ $t('fixVal') }}
<b-popover
target="buttonFCV"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('fixValPop')"
/>
</button>
<button
v-if="user.preferences.disableClasses == true"
id="buttonEnableClasses"
class="btn btn-primary mb-2"
@click="changeClassForUser(false)"
>
{{ $t('enableClass') }}
<b-popover
target="buttonEnableClasses"
triggers="hover focus"
placement="right"
:prevent-overflow="false"
:content="$t('enableClassPop')"
/>
</button>
<hr>
<day-start-adjustment />
</div>
</div>
<div class="col-sm-6">
<h2>{{ $t('registration') }}</h2>
<div class="panel-body">
<div>
<ul class="list-inline">
<li
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
>
<button
v-if="!user.auth[network.key].id && network.key !== 'facebook'"
class="btn btn-primary mb-2"
@click="socialAuth(network.key, user)"
>
{{ $t('registerWithSocial', {network: network.name}) }}
</button>
<button
v-if="!hasBackupAuthOption(network.key) && user.auth[network.key].id"
class="btn btn-primary mb-2"
disabled="disabled"
>
{{ $t('registeredWithSocial', {network: network.name}) }}
</button>
<button
v-if="hasBackupAuthOption(network.key) && user.auth[network.key].id"
class="btn btn-danger"
@click="deleteSocialAuth(network)"
>
{{ $t('detachSocial', {network: network.name}) }}
</button>
</li>
</ul>
<hr>
<div v-if="!user.auth.local.has_password">
<h5 v-if="!user.auth.local.email">
{{ $t('addLocalAuth') }}
</h5>
<h5 v-if="user.auth.local.email">
{{ $t('addPasswordAuth') }}
</h5>
<div
class="form"
name="localAuth"
novalidate="novalidate"
>
<div
v-if="!user.auth.local.email"
class="form-group"
>
<input
v-model="localAuth.email"
class="form-control"
type="text"
:placeholder="$t('email')"
required="required"
>
</div>
<div class="form-group">
<input
v-model="localAuth.password"
class="form-control"
type="password"
:placeholder="$t('password')"
required="required"
>
</div>
<div class="form-group">
<input
v-model="localAuth.confirmPassword"
class="form-control"
type="password"
:placeholder="$t('confirmPass')"
required="required"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="addLocalAuth()"
>
{{ $t('submit') }}
</button>
</div>
</div>
</div>
<div class="usersettings">
<h5>{{ $t('changeDisplayName') }}</h5>
<div
class="form"
name="changeDisplayName"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeDisplayname"
v-model="temporaryDisplayName"
class="form-control"
type="text"
:placeholder="$t('newDisplayName')"
:class="{'is-invalid input-invalid': displayNameInvalid}"
>
<div
v-if="displayNameIssues.length > 0"
class="mb-3"
>
<div
v-for="issue in displayNameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
</div>
</div>
<button
class="btn btn-primary"
type="submit"
:disabled="displayNameCannotSubmit"
@click="changeDisplayName(temporaryDisplayName)"
>
{{ $t('submit') }}
</button>
</div>
<h5>{{ $t('changeUsername') }}</h5>
<div
class="form"
name="changeUsername"
novalidate="novalidate"
>
<div
v-if="verifiedUsername"
class="iconalert iconalert-success"
>
{{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
</div>
<div
v-else
class="iconalert iconalert-warning"
>
<div class="align-middle">
<span>{{ $t('usernameNotVerified') }}</span>
</div>
</div>
<div class="form-group">
<input
id="changeUsername"
v-model="usernameUpdates.username"
class="form-control"
type="text"
:placeholder="$t('newUsername')"
:class="{'is-invalid input-invalid': usernameInvalid}"
@blur="restoreEmptyUsername()"
>
<div
v-for="issue in usernameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
<small class="form-text text-muted">{{ $t('changeUsernameDisclaimer') }}</small>
</div>
<button
class="btn btn-primary"
type="submit"
:disabled="usernameCannotSubmit"
@click="changeUser('username', usernameUpdates)"
>
{{ $t('saveAndConfirm') }}
</button>
</div>
<h5 v-if="user.auth.local.has_password">
{{ $t('changeEmail') }}
</h5>
<div
v-if="user.auth.local.email"
class="form"
name="changeEmail"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeEmail"
v-model="emailUpdates.newEmail"
class="form-control"
type="text"
:placeholder="$t('newEmail')"
>
</div>
<div
v-if="user.auth.local.has_password"
class="form-group"
>
<input
v-model="emailUpdates.password"
class="form-control"
type="password"
:placeholder="$t('password')"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="changeUser('email', emailUpdates)"
>
{{ $t('submit') }}
</button>
</div>
<h5 v-if="user.auth.local.has_password">
{{ $t('changePass') }}
</h5>
<div
v-if="user.auth.local.has_password"
class="form"
name="changePassword"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changePassword"
v-model="passwordUpdates.password"
class="form-control"
type="password"
:placeholder="$t('oldPass')"
>
</div>
<div class="form-group">
<input
v-model="passwordUpdates.newPassword"
class="form-control"
type="password"
:placeholder="$t('newPass')"
>
</div>
<div class="form-group">
<input
v-model="passwordUpdates.confirmPassword"
class="form-control"
type="password"
:placeholder="$t('confirmPass')"
>
</div>
<button
class="btn btn-primary"
type="submit"
@click="changeUser('password', passwordUpdates)"
>
{{ $t('submit') }}
</button>
</div>
<hr>
</div>
<div>
<h5>{{ $t('dangerZone') }}</h5>
<div>
<button
v-b-popover.hover.auto="$t('resetAccPop')"
class="btn btn-danger mr-2 mb-2"
popover-trigger="mouseenter"
popover-placement="right"
@click="openResetModal()"
>
{{ $t('resetAccount') }}
</button>
<button
v-b-popover.hover.auto="$t('deleteAccPop')"
class="btn btn-danger mb-2"
popover-trigger="mouseenter"
@click="openDeleteModal()"
>
{{ $t('deleteAccount') }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
color: $gray-50;
}
.checkbox {
width: fit-content;
}
.usersettings h5 {
margin-top: 1em;
}
.iconalert > div > span {
line-height: 25px;
}
.iconalert > div:after {
clear: both;
content: '';
display: table;
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
margin-top: 5px;
}
.sleep {
margin-bottom: 16px;
}
</style>
<script>
import hello from 'hellojs';
import axios from 'axios';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
import restoreModal from './restoreModal';
import resetModal from './resetModal';
import deleteModal from './deleteModal';
import dayStartAdjustment from './dayStartAdjustment';
import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
import changeClass from '@/../../common/script/ops/changeClass';
import notificationsMixin from '../../mixins/notifications';
import sounds from '../../libs/sounds';
import { buildAppleAuthUrl } from '../../libs/auth';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
export default {
components: {
restoreModal,
resetModal,
deleteModal,
dayStartAdjustment,
},
mixins: [notificationsMixin],
data () {
return {
SOCIAL_AUTH_NETWORKS: [],
party: {},
// Made available by the server as a script
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
temporaryDisplayName: '',
usernameUpdates: { username: '' },
emailUpdates: {},
passwordUpdates: {},
localAuth: {
username: '',
email: '',
password: '',
confirmPassword: '',
},
displayNameIssues: [],
usernameIssues: [],
};
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
availableAudioThemes () {
return ['off', ...this.content.audioThemes];
},
hasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
verifiedUsername () {
return this.user.flags.verifiedUsername;
},
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) return false;
return !this.displayNameValid;
},
displayNameValid () {
if (this.temporaryDisplayName.length <= 1) return false;
return this.displayNameIssues.length === 0;
},
displayNameCannotSubmit () {
if (this.temporaryDisplayName.length <= 1) return true;
return !this.displayNameValid;
},
usernameValid () {
if (this.usernameUpdates.username.length <= 1) return false;
return this.usernameIssues.length === 0;
},
usernameInvalid () {
if (this.usernameUpdates.username.length <= 1) return false;
return !this.usernameValid;
},
usernameCannotSubmit () {
if (this.usernameUpdates.username.length <= 1) return true;
return !this.usernameValid;
},
},
watch: {
usernameUpdates: {
handler () {
this.validateUsername(this.usernameUpdates.username);
},
deep: true,
},
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
},
mounted () {
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
// @TODO: We may need to request the party here
this.party = this.$store.state.party;
this.usernameUpdates.username = this.user.auth.local.username || null;
this.temporaryDisplayName = this.user.profile.name;
this.emailUpdates.newEmail = this.user.auth.local.email || null;
this.localAuth.username = this.user.auth.local.username || null;
this.soundIndex = 0;
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
});
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
}, {
redirect_uri: '', // eslint-disable-line
});
const focusID = this.$route.query.focus;
if (focusID !== undefined && focusID !== null) {
this.$nextTick(() => {
const element = document.getElementById(focusID);
if (element !== undefined && element !== null) {
element.focus();
}
});
}
},
methods: {
toggleSleep () {
this.$store.dispatch('user:sleep');
},
validateDisplayName: debounce(function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
this.$store.dispatch('auth:verifyDisplayName', {
displayName,
}).then(res => {
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
});
}, 500),
validateUsername: debounce(function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
this.$store.dispatch('auth:verifyUsername', {
username,
}).then(res => {
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
});
}, 500),
set (preferenceType, subtype) {
const settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
return this.$store.dispatch('user:set', settings);
},
hideHeader () {
this.set('hideHeader');
if (!this.user.preferences.hideHeader || !this.user.preferences.stickyHeader) return;
this.user.preferences.hideHeader = false;
this.set('stickyHeader');
},
toggleStickyHeader () {
this.set('stickyHeader');
},
showTour () {
// @TODO: Do we still use this?
// User.set({'flags.showTour':true});
// Guide.goto('intro', 0, true);
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
hasBackupAuthOption (networkKeyToCheck) {
if (this.user.auth.local.username) {
return true;
}
return this.SOCIAL_AUTH_NETWORKS.find(network => {
if (network.key !== networkKeyToCheck) {
if (this.user.auth[network.key]) {
return !!this.user.auth[network.key].id;
}
}
return false;
});
},
async changeLanguage (e) {
const newLang = e.target.value;
this.user.preferences.language = newLang;
await this.set('language');
setTimeout(() => window.location.reload(true));
},
async changeUser (attribute, updates) {
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
if (attribute === 'username') {
this.user.auth.local.username = updates[attribute];
this.localAuth.username = this.user.auth.local.username;
this.user.flags.verifiedUsername = true;
} else if (attribute === 'email') {
this.user.auth.local.email = updates.newEmail;
window.alert(this.$t('emailSuccess')); // eslint-disable-line no-alert
} else if (attribute === 'password') {
this.passwordUpdates = {};
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('passwordSuccess'),
type: 'success',
timeout: true,
});
}
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', { 'profile.name': newName });
window.alert(this.$t('displayNameSuccess')); // eslint-disable-line no-alert
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
},
openRestoreModal () {
this.$root.$emit('bv::show::modal', 'restore');
},
openResetModal () {
this.$root.$emit('bv::show::modal', 'reset');
},
openDeleteModal () {
this.$root.$emit('bv::show::modal', 'delete');
},
async deleteSocialAuth (network) {
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
this.user.auth[network.key] = {};
this.text(this.$t('detachedSocial', { network: network.name }));
},
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
const auth = await hello(network).login({ scope: 'email' });
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
}
},
async changeClassForUser (confirmationNeeded) {
if (confirmationNeeded && !window.confirm(this.$t('changeClassConfirmCost'))) return; // eslint-disable-line no-alert
try {
changeClass(this.user);
await axios.post('/api/v4/user/change-class');
} catch (e) {
window.alert(e.message); // eslint-disable-line no-alert
}
},
async addLocalAuth () {
if (this.localAuth.email === '') {
this.localAuth.email = this.user.auth.local.email;
}
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
window.location.href = '/user/settings/site';
},
restoreEmptyUsername () {
if (this.usernameUpdates.username.length < 1) {
this.usernameUpdates.username = this.user.auth.local.username;
}
},
changeAudioTheme () {
this.soundIndex = 0;
this.set('sound');
},
playAudio () {
this.$root.$emit('playSound', sounds[this.soundIndex]);
this.soundIndex = (this.soundIndex + 1) % sounds.length;
},
},
};
</script>

View file

@ -115,8 +115,6 @@
}
.input-error {
color: $red-50;
font-size: 90%;
width: 100%;
}

View file

@ -35,7 +35,7 @@
:key="item.key"
:item="item"
:price="item.value"
:item-content-class="'shop_'+item.key"
:item-content-class="`shop_${item.key}`"
:empty-item="false"
:popover-position="'top'"
@click="featuredItemSelected(item)"

View file

@ -0,0 +1,54 @@
<template>
<div
v-once
class="gem-price-div"
:class="{'background': withBackground}"
>
<div
:class="`mr-2 svg-icon gem icon-${iconSize}`"
v-html="icons.gem"
></div>
<span class="gem-price">{{ gemPrice }}</span>
</div>
</template>
<script>
import gemIcon from '@/assets/svg/gem.svg';
export default {
name: 'GemPrice',
props: ['gemPrice', 'iconSize', 'withBackground'],
data () {
return {
icons: Object.freeze({
gem: gemIcon,
}),
};
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.gem-price {
font-size: 20px;
font-weight: bold;
line-height: 1.4;
color: $green-10;
}
.gem-price-div {
display: inline-flex;
align-items: center;
}
.background {
align-self: center;
border-radius: 20px;
padding: 6px 20px;
background-color: rgba($green-100, 0.15);
}
</style>

View file

@ -92,7 +92,7 @@
:item="item"
:price="item.goldValue ? item.goldValue : item.value"
:price-type="item.goldValue ? 'gold' : 'gem'"
:item-content-class="'inventory_quest_scroll_'+item.key"
:item-content-class="`inventory_quest_scroll_${item.key}`"
:empty-item="false"
:popover-position="'top'"
@click="selectItem(item)"

View file

@ -82,7 +82,7 @@
<item-with-label
v-for="drop in getDropsList(quest.drop.items, false)"
:key="drop.type+'_'+drop.key"
:key="`${drop.type}_${drop.key}`"
:item="{}"
class="item-with-label"
>

View file

@ -880,8 +880,6 @@ export default {
},
mounted () {
hello.init({
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
this.$store.dispatch('common:setTitle', {

View file

@ -682,7 +682,9 @@ export default {
// as default filter for daily
// and set the filter as 'due' only when the component first
// loads and not on subsequent reloads.
if (type === 'daily' && filter === '' && !this.challenge) {
if (
type === 'daily' && filter === '' && !this.challenge
) {
filter = 'due'; // eslint-disable-line no-param-reassign
}

View file

@ -13,7 +13,6 @@
</span>
<label
v-once
class="mb-1"
v-html="text"
></label>
</div>
@ -23,11 +22,11 @@
@import '~@/assets/scss/colors.scss';
label {
height: 1.5rem;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
letter-spacing: normal;
margin: 0;
}
.gray-200 {

View file

@ -2,7 +2,6 @@
<div>
<select-list
:items="items"
:key-prop="'icon'"
class="difficulty-select"
:class="{disabled: disabled}"
:disabled="disabled"
@ -10,7 +9,7 @@
:hide-icon="true"
@select="$emit('select', $event.value)"
>
<template v-slot:item="{ item, button }">
<template #item="{ item, button }">
<div
v-if="item"
class="difficulty-item"

View file

@ -203,16 +203,15 @@
<template
v-if="task.type !== 'reward'"
>
<div class="d-flex mt-3">
<div class="d-flex mt-3 align-items-center">
<lockable-label
:locked="challengeAccessRequired"
:text="$t('difficulty')"
/>
<div
v-b-tooltip.hover.righttop="$t('difficultyHelp')"
class="svg-icon info-icon mb-auto ml-1"
v-html="icons.information"
></div>
<information-icon
tooltip-id="difficultyHelp"
:tooltip="$t('difficultyHelp')"
/>
</div>
<select-difficulty
:value="task.priority"
@ -452,7 +451,7 @@
>
<div>
<div
v-if="task.type === 'daily' && isUserTask && purpose === 'edit'"
v-if="advancedSettingsShowRestoreStreak"
class="option mt-3"
>
<div class="form-group">
@ -479,8 +478,7 @@
</div>
</div>
<div
v-if="task.type === 'habit'
&& isUserTask && purpose === 'edit' && (task.up || task.down)"
v-if="advancedSettingsShowAdjustCounter"
class="option mt-3"
>
<div class="form-group">
@ -539,6 +537,31 @@
</div>
</div>
</div>
<div
v-if="advancedSettingsShowTaskAlias"
class="option mt-3"
>
<div class="form-group">
<label
v-once
class="mb-1"
>{{ $t('taskAlias') }}
<information-icon
tooltip-id="taskAlias"
:tooltip="$t('taskAliasPopover')"
/>
</label>
<div class="input-group">
<input
v-model="task.alias"
class="form-control"
:placeholder="$t('taskAliasPlaceholder')"
type="text"
>
</div>
</div>
</div>
</div>
</b-collapse>
</div>
@ -882,6 +905,11 @@
height: 1rem;
}
label {
display: inline-flex;
align-items: center;
}
.habit-option {
&-container {
min-width: 3rem;
@ -997,7 +1025,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg';
import streakIcon from '@/assets/svg/streak.svg';
@ -1006,10 +1033,12 @@ import goldIcon from '@/assets/svg/gold.svg';
import chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default {
components: {
InformationIcon,
SelectMulti,
Datepicker,
checklist,
@ -1029,7 +1058,6 @@ export default {
showAssignedSelect: false,
newChecklistItem: null,
icons: Object.freeze({
information: informationIcon,
negative: negativeIcon,
positive: positiveIcon,
destroy: deleteIcon,
@ -1064,25 +1092,25 @@ export default {
dayMapping: 'constants.DAY_MAPPING',
ATTRIBUTES: 'constants.ATTRIBUTES',
}),
advancedSettingsAvailable () {
if (
this.task.type === 'reward'
|| this.task.type === 'todo'
|| this.purpose === 'create'
|| !this.isUserTask
) {
return false;
}
if (this.task.type === 'habit'
&& !this.task.up
&& !this.task.down
) {
return false;
}
return true;
// region advanced settings
advancedSettingsShowAdjustCounter () {
return this.task.type === 'habit'
&& this.isUserTask && this.purpose === 'edit'
&& (this.task.up || this.task.down);
},
advancedSettingsShowRestoreStreak () {
return this.task.type === 'daily' && this.isUserTask
&& this.purpose === 'edit';
},
advancedSettingsShowTaskAlias () {
return this.isUserTask && this.user.preferences.developerMode;
},
advancedSettingsAvailable () {
return this.advancedSettingsShowRestoreStreak
|| this.advancedSettingsShowAdjustCounter
|| this.advancedSettingsShowTaskAlias;
},
// endregion advanced settings
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
@ -1157,7 +1185,6 @@ export default {
},
},
async mounted () {
this.showAdvancedOptions = !this.user.preferences.advancedCollapsed;
if (this.groupId) {
const groupResponse = await axios.get(`/api/v4/groups/${this.groupId}`);
this.managers = Object.keys(groupResponse.data.data.managers);

View file

@ -0,0 +1,37 @@
<template>
<span class="ml-1">
<div
:id="`tooltip_${tooltipId}`"
class="svg-icon icon-16"
:title="tooltip"
v-html="icons.information"
></div>
<b-tooltip
:title="tooltip"
:target="`tooltip_${tooltipId}`"
/>
</span>
</template>
<style lang="scss" scoped>
span {
display: inline-block;
vertical-align: middle;
}
</style>
<script>
import informationIcon from '@/assets/svg/information.svg';
export default {
name: 'InformationIcon',
props: ['tooltipId', 'tooltip'],
data () {
return {
icons: Object.freeze({
information: informationIcon,
}),
};
},
};
</script>

View file

@ -9,7 +9,7 @@
@show="isOpened = true"
@hide="isOpened = false"
>
<template v-slot:button-content>
<template #button-content>
<slot
name="item"
:item="selected || placeholder"
@ -21,13 +21,13 @@
</template>
<b-dropdown-item
v-for="item in items"
:key="keyProp ? item[keyProp] : item"
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]"
:active="item === selected"
:key="getKeyProp(item)"
:disabled="isDisabled(item)"
:active="isSelected(item)"
:class="{
active: item === selected,
active: isSelected(item),
selectListItem: true,
showIcon: !hideIcon && item === selected
showIcon: !hideIcon && isSelected(item)
}"
@click="selectItem(item)"
>
@ -51,39 +51,39 @@
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/colors.scss';
.select-list ::v-deep {
.dropdown-toggle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
.select-list ::v-deep {
.dropdown-toggle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
}
.selectListItem {
position: relative;
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.selectListItem {
position: relative;
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
&:not(.showIcon) {
.svg-icon.check-icon {
display: none;
}
}
&:not(.showIcon) {
.svg-icon.check-icon {
display: none;
}
}
.svg-icon.check-icon.color {
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
width: 0.77rem;
height: 0.615rem;
color: $purple-300;
}
.svg-icon.check-icon.color {
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
width: 0.77rem;
height: 0.615rem;
color: $purple-300;
}
}
}
</style>
<script>
@ -101,6 +101,9 @@ export default {
keyProp: {
type: String,
},
activeKeyProp: {
type: String,
},
disabledProp: {
type: String,
},
@ -128,10 +131,23 @@ export default {
};
},
methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item;
},
isDisabled (item) {
return typeof item[this.disabledProp] === 'undefined' ? false : item[this.disabledProp];
},
selectItem (item) {
this.selected = item;
this.selected = this.getKeyProp(item);
this.$emit('select', item);
},
isSelected (item) {
if (this.activeKeyProp) {
return item[this.activeKeyProp] === this.selected;
}
return item === this.selected;
},
},
};
</script>

View file

@ -0,0 +1,131 @@
<template>
<div>
<div class="label-line">
<div
v-if="settingsLabel"
class="settings-label"
>
{{ $t(settingsLabel) }}
</div>
<slot name="top-right"></slot>
</div>
<div class="form-group">
<div
class="input-group"
:class="{
'is-valid': validStyle,
'is-invalid': invalidStyle
}"
>
<input
:value="value"
class="form-control"
:type="inputType"
:class="{
'is-invalid input-invalid': invalidStyle,
'is-valid input-valid': validStyle
}"
:readonly="readonly"
:aria-readonly="readonly"
:placeholder="placeholder"
@keyup="handleChange"
@blur="$emit('blur')"
>
</div>
<div
v-for="issue in invalidIssues"
:key="issue"
class="input-error"
>
{{ issue }} &nbsp;
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ValidatedTextInput',
model: {
prop: 'value',
event: 'update:value',
},
props: {
value: {
type: String,
default: '',
},
isValid: {
type: Boolean,
default: false,
},
onlyShowInvalidState: {
type: Boolean,
default: false,
},
inputType: {
type: String,
default: 'text',
},
readonly: {
type: Boolean,
default: false,
},
settingsLabel: {
type: String,
},
placeholder: {
type: String,
},
invalidIssues: {
type: Array,
default: () => [],
},
},
data () {
return {
wasChanged: false,
};
},
computed: {
canChangeClasses () {
return !this.readonly && this.wasChanged;
},
validStyle () {
return this.canChangeClasses && this.isValid && !this.onlyShowInvalidState;
},
invalidStyle () {
return this.canChangeClasses && !this.isValid;
},
},
methods: {
handleChange ({ target: { value } }) {
this.wasChanged = true;
this.$emit('update:value', value);
},
},
};
</script>
<style lang="scss" scoped>
.label-line {
display: flex;
}
.settings-label {
flex: 1;
}
.input-error {
margin-top: 0.5rem;
}
.form-group {
margin-bottom: 0;
}
</style>

View file

@ -1,5 +1,6 @@
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import {
setup as setupAnalytics,
@ -28,6 +29,7 @@ Vue.config.productionTip = IS_PRODUCTION;
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
Vue.use(StoreModule);
Vue.use(BootstrapVue);
Vue.use(Fragment.Plugin);
setUpLogging();
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time

View file

@ -0,0 +1,23 @@
import notifications from './notifications';
export default {
mixins: [notifications],
methods: {
async mixinCopyToClipboard (valueToCopy, notificationToShow = null) {
if (navigator.clipboard) {
await navigator.clipboard.writeText(valueToCopy);
} else {
// fallback if clipboard API does not exist
const copyText = document.createElement('textarea');
copyText.value = valueToCopy;
document.body.appendChild(copyText);
copyText.select();
document.execCommand('copy');
document.body.removeChild(copyText);
}
if (notificationToShow) {
this.text(notificationToShow);
}
},
},
};

View file

@ -10,7 +10,7 @@ function toFixedWithoutRounding (num, fixed) {
return num.toString().match(re)[0];
}
export default {
export const NotificationMixins = {
computed: {
...mapState({ notifications: 'notificationStore' }),
},
@ -90,3 +90,5 @@ export default {
},
},
};
export default NotificationMixins;

View file

@ -0,0 +1,62 @@
/**
* Component Example
*
* <current-password-input
* :show-forget-password="true"
* :is-valid="mixinData.currentPasswordIssues.length === 0"
* :invalid-issues="mixinData.currentPasswordIssues"
* @passwordValue="updates.password = $event"
* />
*/
export const PasswordInputChecksMixin = {
data () {
return {
mixinData: {
currentPasswordIssues: [],
newPasswordIssues: [],
confirmPasswordIssues: [],
},
};
},
methods: {
clearPasswordIssues () {
this.mixinData.currentPasswordIssues.length = 0;
this.mixinData.newPasswordIssues.length = 0;
this.mixinData.confirmPasswordIssues.length = 0;
},
/**
* @param {() => Promise<void>} promiseCall
* @returns {Promise<void>}
*/
async passwordInputCheckMixinTryCall (promiseCall) {
try {
// reset previous issues
this.clearPasswordIssues();
await promiseCall();
} catch (axiosError) {
const message = axiosError.response?.data?.message;
if ([this.$t('wrongPassword'), this.$t('missingPassword')].includes(message)) {
this.mixinData.currentPasswordIssues.push(message);
} else if ([this.$t('missingNewPassword'), this.$t('passwordIssueLength'), this.$t('passwordConfirmationMatch')].includes(message)) {
this.mixinData.newPasswordIssues.push(message);
this.mixinData.confirmPasswordIssues.push(message);
} else if (this.$t('invalidReqParams') === message) {
const errors = axiosError.response?.data?.errors ?? [];
for (const error of errors) {
if (error.param === 'password') {
this.mixinData.currentPasswordIssues.push(error.message);
} else if (error.param === 'newPassword') {
this.mixinData.newPasswordIssues.push(error.message);
} else {
this.mixinData.confirmPasswordIssues.push(error.message);
}
}
}
}
},
},
};

View file

@ -3,7 +3,7 @@ import { mapState } from '@/libs/store';
export const userCustomStateMixin = fieldname => {
const map = { };
map[fieldname] = 'user.data';
return { // eslint-disable-line import/prefer-default-export
return {
computed: {
...mapState(map),
},

View file

@ -0,0 +1,238 @@
<template>
<div class="row">
<secondary-menu class="col-12">
<template
v-for="routePath in tabs"
>
<router-link
v-if="allowedToShowTab(routePath)"
:key="routePath"
class="nav-link"
:to="{name: routePath}"
exact="exact"
:class="{'active': $route.name === routePath}"
>
{{ $t(pathTranslateKey(routePath)) }}
</router-link>
</template>
</secondary-menu>
<div
v-if="$route.name === 'subscription' && promo === 'g1g1'"
class="g1g1-banner d-flex justify-content-center"
@click="showSelectUser"
>
<div
v-once
class="svg-icon svg-gifts left-gift"
v-html="icons.gifts"
>
</div>
<div class="d-flex flex-column align-items-center text-center">
<strong
class="mt-auto mb-1"
> {{ $t('g1g1Event') }} </strong>
<p
class="mb-auto"
>
{{ $t('g1g1Details') }}
</p>
</div>
<div
v-once
class="svg-icon svg-gifts right-gift"
v-html="icons.gifts"
>
</div>
</div>
<div
class="col-12 d-flex "
:class="{'justify-content-center': applyNarrowView}"
>
<div :class="{'settings-content': applyNarrowView, 'full-width-content': !applyNarrowView}">
<router-view />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
strong {
font-size: 1rem;
line-height: 1.25;
}
.g1g1-banner {
color: $white;
width: 100%;
height: 5.75rem;
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
cursor: pointer;
}
.left-gift {
margin: auto 3rem auto auto;
}
.right-gift {
margin: auto auto auto 3rem;
filter: flipH;
transform: scaleX(-1);
}
.svg-gifts {
width: 3.5rem;
}
.full-width-content {
width: 100%;
margin-left: 10%;
margin-right: 10%;
}
.settings-content {
flex: 0 0 732px;
max-width: unset;
::v-deep {
line-height: 1.71;
.small {
line-height: 1.33;
}
table td {
padding: 0.5rem;
}
table tr.expanded td {
padding-bottom: 1.5rem;
}
.settings-label {
font-weight: bold;
color: $gray-50;
width: 23%;
}
.input-area .settings-label {
width: unset;
}
.settings-value {
color: $gray-50;
width: auto;
}
.settings-button {
width: 30%;
text-align: end;
}
.dialog-title {
font-size: 14px;
font-weight: bold;
color: $purple-300;
&.danger {
color: $maroon-50;
}
}
.dialog-disclaimer {
color: $gray-50;
}
.input-area {
width: 320px;
margin: 1rem auto 0;
}
.edit-link {
&:hover {
text-decoration: underline;
}
}
.remove-link {
color: $maroon-50 !important;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<script>
import find from 'lodash/find';
import { mapState } from '@/libs/store';
import SecondaryMenu from '@/components/secondaryMenu';
import gifts from '@/assets/svg/gifts-vertical.svg';
import { userStateMixin } from '@/mixins/userState';
export default {
components: {
SecondaryMenu,
},
mixins: [userStateMixin],
data () {
return {
icons: Object.freeze({
gifts,
}),
tabs: [
'general',
'subscription',
'siteData',
'promoCode',
'transactions',
'notifications',
],
};
},
computed: {
...mapState({
currentEventList: 'worldState.data.currentEventList',
}),
currentEvent () {
return find(this.currentEventList, event => Boolean(event.promo));
},
promo () {
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
return this.currentEvent.promo;
},
applyNarrowView () {
return !['subscription', 'transactions'].includes(this.$route.name);
},
},
methods: {
/**
* @param {String} tabName
* @returns {Boolean}
*/
allowedToShowTab (tabName) {
const transactionsTab = tabName === 'transactions';
return transactionsTab
? this.hasPermission(this.user, 'userSupport')
: true;
},
showSelectUser () {
this.$root.$emit('bv::show::modal', 'select-user-modal');
},
pathTranslateKey (path) {
if (path === 'api') {
return 'API';
}
return path;
},
},
};
</script>

View file

@ -0,0 +1,91 @@
<template>
<div
class="class-value"
:class="{[selectedClass]: !classDisabled, disabled: classDisabled}"
>
<span
v-if="!classDisabled"
class="svg-icon icon-16 mr-2"
v-html="classIcons[selectedClass]"
></span>
<span
v-if="classDisabled"
class="label"
>
{{ $t('noClassSelected') }}
</span>
<span
v-else
class="label"
>
{{ $t(selectedClass) }}
</span>
</div>
</template>
<script>
import warriorIcon from '@/assets/svg/warrior.svg';
import rogueIcon from '@/assets/svg/rogue.svg';
import healerIcon from '@/assets/svg/healer.svg';
import wizardIcon from '@/assets/svg/wizard.svg';
export default {
name: 'ClassIconLabel',
props: ['selectedClass', 'classDisabled'],
data () {
return {
classIcons: Object.freeze({
warrior: warriorIcon,
rogue: rogueIcon,
healer: healerIcon,
wizard: wizardIcon,
}),
};
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.class-value {
display: flex;
align-items: center;
&:not(.disabled) {
.label {
font-weight: bold;
line-height: 1.71;
}
}
}
.healer {
color: $healer-color;
}
.rogue {
color: $rogue-color;
}
.warrior {
color: $warrior-color;
}
.wizard {
color: $wizard-color;
}
.disabled {
color: $maroon-50;
}
.label {
font-size: 14px;
line-height: 1.71;
text-align: center;
}
</style>

View file

@ -0,0 +1,53 @@
<template>
<div class="input-area">
<validated-text-input
v-model="currentPassword"
:settings-label="customLabel ?? 'password'"
:placeholder="$t(customLabel ?? 'password')"
:is-valid="isValid"
:invalid-issues="invalidIssues"
:only-show-invalid-state="true"
input-type="password"
@update:value="$emit('passwordValue', currentPassword)"
>
<div
v-if="showForgetPassword"
slot="top-right"
class="forgot-password"
>
<router-link
to="/forgot-password"
target="_blank"
>
{{ $t('forgotPassword') }}
</router-link>
</div>
</validated-text-input>
</div>
</template>
<script>
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
export default {
name: 'CurrentPasswordInput',
components: { ValidatedTextInput },
props: ['customLabel', 'showForgetPassword', 'isValid', 'invalidIssues'],
data () {
return {
currentPassword: '',
};
},
};
</script>
<style lang="scss" scoped>
.forgot-password {
a {
font-size: 12px;
line-height: 1.33;
}
}
</style>

View file

@ -0,0 +1,13 @@
export const GenericUserPreferencesMixin = {
methods: {
setUserPreference (preferenceType, subtype) {
const settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
return this.$store.dispatch('user:set', settings);
},
},
};

View file

@ -0,0 +1,83 @@
import { reactive } from 'vue';
export const sharedInlineSettingStore = reactive({
inlineSettingAlreadyOpen: false,
inlineSettingUnsavedValues: false,
/**
* @type InlineSettingMixin
*/
instanceOfCurrentlyOpened: null,
markAsOpened (currentInstance) {
this.inlineSettingAlreadyOpen = true;
this.instanceOfCurrentlyOpened = currentInstance;
},
markAsClosed () {
this.inlineSettingUnsavedValues = false;
this.inlineSettingAlreadyOpen = false;
},
});
export const InlineSettingMixin = {
data () {
return {
mixinData: {
inlineSettingMixin: {
modalVisible: false,
sharedState: sharedInlineSettingStore,
},
},
};
},
methods: {
openModal () {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingAlreadyOpen) {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
if (window.confirm(this.$t('confirmCancelChanges'))) {
this._hidePrevious();
this._openIt();
} else {
return;
}
} else {
this._hidePrevious();
}
}
this._openIt();
},
_openIt () {
this.mixinData.inlineSettingMixin.sharedState.markAsOpened(this);
this.mixinData.inlineSettingMixin.modalVisible = true;
this.$el.scrollTo({
behavior: 'smooth',
});
},
_hidePrevious () {
this.mixinData.inlineSettingMixin.sharedState.instanceOfCurrentlyOpened.resetControls();
this.mixinData.inlineSettingMixin.sharedState.instanceOfCurrentlyOpened.closeModal();
},
/**
* This is just for the cancel buttons - so that they also ask if there are unchanged values
*/
requestCloseModal () {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues && !window.confirm(this.$t('confirmCancelChanges'))) {
return;
}
this.resetControls();
this.closeModal();
},
/**
* This is for the save methods to call it after they are done
*/
closeModal () {
this.mixinData.inlineSettingMixin.modalVisible = false;
this.mixinData.inlineSettingMixin.sharedState.markAsClosed();
},
modalValuesChanged (value = true) {
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = value;
},
resetControls () {},
},
};

View file

@ -0,0 +1,100 @@
<template>
<div class="input-area">
<div class="label-line">
<div class="settings-label">
{{ label }}
</div>
<div
class="link-style"
@click="mixinCopyToClipboard(value, notificationText)"
>
{{ $t('copy') }}
</div>
</div>
<div class="form-group">
<div
class="input-group"
>
<div class="input-group-prepend input-group-icon">
<div
v-once
class="svg-icon icon-16"
v-html="icons.lock"
></div>
</div>
<input
:value="value"
class="form-control"
readonly
aria-readonly="true"
type="text"
>
</div>
</div>
</div>
</template>
<script>
import CopyToClipboard from '@/mixins/copyToClipboard';
import svgLockSmall from '@/assets/svg/lock-small.svg';
export default {
name: 'LockedInput',
mixins: [CopyToClipboard],
props: ['label', 'value', 'notificationText'],
data () {
return {
icons: Object.freeze({
lock: svgLockSmall,
}),
};
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.label-line {
display: flex;
}
.settings-label {
flex: 1;
}
.link-style {
font-size: 12px;
line-height: 1.33;
color: $purple-300;
cursor: pointer;
display: flex;
align-items: center;
&:hover, &:active, &:focus {
text-decoration: underline;
}
}
.input-group {
border-radius: 2px;
input {
border: solid 1px $gray-500;
background-color: $gray-700;
&:hover {
outline: 0;
}
}
}
.input-group-icon {
padding: 8px;
border-radius: 2px;
background-color: $gray-600;
}
</style>

View file

@ -0,0 +1,68 @@
<template>
<div
class="buttons"
:class="{'no-padding': noPadding}"
>
<button
v-if="!hideSave"
class="btn btn-save"
:class="primaryButtonColor ?? 'btn-primary'"
type="submit"
:disabled="disableSave"
@click="$emit('saveClicked')"
>
{{ $t(primaryButtonLabel ?? 'save') }}
</button>
<a
v-if="!hideCancel"
class="edit-link"
@click.prevent="$emit('cancelClicked')"
>
{{ $t('cancel') }}
</a>
</div>
</template>
<script>
export default {
name: 'SaveCancelButtons',
props: {
hideSave: {
type: Boolean,
},
hideCancel: {
type: Boolean,
},
disableSave: {
type: Boolean,
},
noPadding: {
type: Boolean,
},
primaryButtonLabel: {
type: String,
},
primaryButtonColor: {
type: String,
},
},
};
</script>
<style lang="scss" scoped>
.buttons {
align-items: center;
display: flex;
flex-direction: column;
&:not(.no-padding) {
margin-top: 1.5rem;
}
}
.btn-save {
margin-bottom: 1rem;
}
</style>

View file

@ -0,0 +1,56 @@
<template>
<div class="your-balance">
<span
v-once
class="label"
>
{{ $t('yourBalance') }}
</span>
<balance-info
class="balance-info"
:currency-needed="currencyNeeded"
:amount-needed="amountNeeded"
/>
</div>
</template>
<script>
import BalanceInfo from '@/components/shops/balanceInfo.vue';
export default {
name: 'YourBalance',
components: { BalanceInfo },
props: {
currencyNeeded: {
type: String,
},
amountNeeded: {
type: Number,
},
},
};
</script>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.your-balance {
padding: 8px 16px;
border-radius: 4px;
background-color: $gray-600;
display: inline-block;
align-self: center;
}
.label {
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
}
.balance-info {
display: inline-block !important;
}
</style>

View file

@ -0,0 +1,140 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('generalSettings') }}
</h1>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('account') }}
</h2>
<table class="table">
<user-name-setting />
<user-email-setting />
<display-name-setting />
<password-setting />
<reset-account />
<delete-account />
<tr>
<td colspan="3"></td>
</tr>
</table>
<h2 v-once>
{{ $t('loginMethods') }}
</h2>
<table class="table">
<LoginMethods />
<tr>
<td colspan="3">
</td>
</tr>
</table>
<h2 v-once>
{{ $t('site') }}
</h2>
<table class="table">
<language-setting />
<date-format-setting />
<day-start-adjustment-setting />
<audio-theme-setting />
<sleep-mode />
<tr>
<td colspan="3">
</td>
</tr>
</table>
<h2 v-once>
{{ $t('character') }}
</h2>
<table class="table">
<fix-values-setting />
<class-setting />
<tr>
<td colspan="3">
</td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.standard-page {
padding-left: 0;
padding-right: 0;
}
.table {
color: $gray-50;
}
</style>
<script>
import notificationsMixin from '../../mixins/notifications';
import UserNameSetting from './settingRows/userNameSetting';
import UserEmailSetting from './settingRows/userEmailSetting';
import DisplayNameSetting from './settingRows/displayNameSetting';
import PasswordSetting from './settingRows/passwordSetting';
import ResetAccount from './settingRows/resetAccount';
import DeleteAccount from './settingRows/deleteAccount';
import { sharedInlineSettingStore } from './components/inlineSettingMixin';
import LanguageSetting from './settingRows/languageSetting';
import DateFormatSetting from './settingRows/dateFormatSetting';
import DayStartAdjustmentSetting from './settingRows/dayStartAdjustmentSetting.vue';
import AudioThemeSetting from '@/pages/settings/settingRows/audioThemeSetting.vue';
import ClassSetting from '@/pages/settings/settingRows/classSetting.vue';
import FixValuesSetting from '@/pages/settings/settingRows/fixValuesSetting.vue';
import LoginMethods from '@/pages/settings/settingRows/loginMethods.vue';
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
import { mapState } from '@/libs/store';
import SleepMode from '@/pages/settings/settingRows/sleepMode.vue';
export default {
components: {
SleepMode,
LoginMethods,
FixValuesSetting,
ClassSetting,
AudioThemeSetting,
DayStartAdjustmentSetting,
DateFormatSetting,
LanguageSetting,
DeleteAccount,
ResetAccount,
PasswordSetting,
DisplayNameSetting,
UserEmailSetting,
UserNameSetting,
},
mixins: [notificationsMixin, GenericUserPreferencesMixin],
computed: {
...mapState({
user: 'user.data',
}),
},
beforeRouteLeave (_, __, next) {
sharedInlineSettingStore.markAsClosed();
next();
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('generalSettings'),
});
},
};
</script>

View file

@ -0,0 +1,325 @@
<template>
<div class="row standard-page px-0">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('notifications') }}
</h1>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('allNotifications') }}
</h2>
<table class="table">
<tr>
<td class="bold">
{{ $t('unsubscribeAllPush') }}
</td>
<td>
<toggle-switch
:checked="user.preferences.pushNotifications.unsubscribeFromAll"
@change="set('pushNotifications', 'unsubscribeFromAll', $event)"
/>
</td>
</tr>
<tr>
<td>
<span class="bold">{{ $t('unsubscribeAllEmails') }}</span> <br>
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
</td>
<td>
<toggle-switch
:checked="user.preferences.emailNotifications.unsubscribeFromAll"
@change="set('emailNotifications', 'unsubscribeFromAll', $event)"
/>
</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
</div>
<div class="col-12">
<h2>Website</h2>
<table class="table">
<tr>
<td
v-once
class="bold"
>
{{ $t('showLevelUpModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.levelUp"
class="toggle-switch-width"
@change="set('suppressModals', 'levelUp', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showHatchPetModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.hatchPet"
class="toggle-switch-width"
@change="set('suppressModals', 'hatchPet', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showRaisePetModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.raisePet"
class="toggle-switch-width"
@change="set('suppressModals', 'raisePet', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('showStreakModal') }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="!user.preferences.suppressModals.streak"
class="toggle-switch-width"
@change="set('suppressModals', 'streak', !$event)"
/>
</td>
</tr>
<tr>
<td
v-once
class="bold"
>
{{ $t('baileyAnnouncement') }}
</td>
<td class="email_push_col show_bailey_col">
<b-popover
target="viewBaileyLink"
triggers="hover"
placement="right"
:content="$t('showBaileyPop')"
/>
<a
id="viewBaileyLink"
class="show_bailey_link"
@click="showBailey()"
>
{{ $t('view') }}
</a>
</td>
</tr>
<tr>
<td></td>
<td></td>
</tr>
</table>
</div>
<div class="col-12">
<h2>Email & Push</h2>
<table class="table">
<tr>
<td></td>
<th class="email_push_col email_col_padding">
<span v-once>{{ $t('email') }}</span>
</th>
<th class="email_push_col">
<span v-once>{{ $t('push') }}</span>
</th>
</tr>
<tr
v-for="notification in notificationsIds"
:key="notification"
>
<td
v-once
class="bold"
>
{{ $t(notification) }}
</td>
<td class="email_push_col">
<toggle-switch
:checked="user.preferences.emailNotifications[notification]"
class="toggle-switch-width"
@change="set('emailNotifications', notification, $event)"
/>
</td>
<td class="email_push_col">
<toggle-switch
v-if="!onlyEmailsIds.includes(notification)"
:checked="user.preferences.pushNotifications[notification]"
class="toggle-switch-width"
@change="set('pushNotifications', notification, $event)"
/>
</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.toggle-switch-width {
::v-deep {
.toggle-switch {
margin-left: 0;
}
}
}
.email_push_col {
width: 50px;
padding-left: 0 !important;
padding-right: 0 !important;
}
/** Table Styles, maybe can be copied / extracted once more Pages need it */
.table {
margin-bottom: 0.5rem;
}
.table th, .table td {
padding: 0.5rem;
}
.bold {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
small {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
}
.email_col_padding {
padding-right: 70px !important;
}
toggle-switch {
padding-right: 8px;
}
.show_bailey_col {
text-align: right;
}
.show_bailey_link {
padding-right: 8px;
line-height: 1.71;
// color: $blue-10 !important;
&:hover {
text-decoration: underline;
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import notificationsMixin from '@/mixins/notifications';
import ToggleSwitch from '@/components/ui/toggleSwitch';
export default {
components: { ToggleSwitch },
mixins: [notificationsMixin],
data () {
return {
notificationsIds: Object.freeze([
'majorUpdates',
'newPM',
'giftedGems',
'giftedSubscription',
'invitedParty',
'invitedGuild',
'invitedQuest',
'questStarted',
'wonChallenge',
// 'weeklyRecaps',
'kickedGroup',
'onboarding',
'importantAnnouncements',
'subscriptionReminders',
]),
// list of email-only notifications
onlyEmailsIds: Object.freeze([
'kickedGroup',
'importantAnnouncements',
'weeklyRecaps',
'onboarding',
'subscriptionReminders',
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('notifications'),
});
// If ?unsubFrom param is passed with valid email type,
// automatically unsubscribe users from that email and
// show an alert
// A simple object to map the key stored in the db (user.preferences.emailNotification[key])
// to its string id but ONLY when the preferences' key and the string key don't match
const MAP_PREF_TO_EMAIL_STRING = {
importantAnnouncements: 'inactivityEmails',
};
const { unsubFrom } = this.$route.query;
if (unsubFrom) {
await this.$store.dispatch('user:set', {
[`preferences.emailNotifications.${unsubFrom}`]: false,
});
const emailTypeString = this.$t(MAP_PREF_TO_EMAIL_STRING[unsubFrom] || unsubFrom);
this.text(this.$t('correctlyUnsubscribedEmailType', { emailType: emailTypeString }));
}
},
methods: {
set (preferenceType, notification, $event) {
const settings = {};
settings[`preferences.${preferenceType}.${notification}`] = $event ?? this.user.preferences[preferenceType][notification];
this.$store.dispatch('user:set', settings);
},
showBailey () {
this.$root.$emit('bv::show::modal', 'new-stuff');
},
},
};
</script>

View file

@ -0,0 +1,77 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('promoCode') }}
</h1>
<div class="input-area">
<div
class="form-inline"
role="form"
>
<input
v-model="couponCode"
class="form-control w-100"
type="text"
:placeholder="$t('promoPlaceholder')"
>
</div>
<div
v-once
class="small mt-2"
>
{{ $t('couponText') }}
</div>
<save-cancel-buttons
:hide-cancel="true"
primary-button-label="submit"
@saveClicked="enterCoupon()"
/>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import SaveCancelButtons from '@/pages/settings/components/saveCancelButtons.vue';
export default {
components: { SaveCancelButtons },
mixins: [notifications],
data () {
return {
codes: {
event: '',
count: '',
},
couponCode: '',
};
},
computed: {
...mapState({ user: 'user.data', credentials: 'credentials' }),
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('promoCode'),
});
},
methods: {
async enterCoupon () {
const code = await axios.post(`/api/v4/coupons/enter/${this.couponCode}`);
if (!code) return;
this.$store.state.user.data = code.data.data;
this.text(this.$t('promoCodeApplied'));
},
},
};
</script>

View file

@ -9,7 +9,7 @@
<script>
import { mapState } from '@/libs/store';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue';
import PurchaseHistoryTable from '../../components/ui/purchaseHistoryTable.vue';
export default {
components: {

View file

@ -0,0 +1,197 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td
v-once
class="settings-label"
>
{{ $t("audioTheme") }}
</td>
<td class="settings-value">
{{ $t(`audioTheme_${currentAudioTheme}`) }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("audioTheme") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("audioThemeDisclaimer") }}</span>
</div>
<div class="input-area">
<div class="label-columns">
<div class="settings-label">
{{ $t("enableAudio") }}
</div>
<div>
<toggle-switch
:checked="!isDisabled"
@change="toggleAudioThemeOff($event)"
/>
</div>
</div>
<div class="label-columns mb-2">
<div class="settings-label">
{{ $t("audioTheme") }}
</div>
<div v-if="!isDisabled">
<a
class="edit-link"
@click.prevent="playAudio()"
>
{{ $t('playDemoAudio') }}
</a>
</div>
</div>
<div class="form-group">
<select-list
:disabled="isDisabled"
:items="availableAudioThemes"
:value="themeSelected"
@select="changeAudioThemeTemporary($event)"
>
<template #item="{ item, button }">
<span v-if="button">
{{ $t(`audioTheme_${themeSelected}`) }}
</span>
<span v-else>
{{ $t(`audioTheme_${item}`) }}
</span>
</template>
</select-list>
</div>
</div>
<save-cancel-buttons
:disable-save="previousValue === currentAudioTheme"
@saveClicked="changeAudioThemeAndClose()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.label-columns {
display: flex;
&:first-of-type {
margin-bottom: 1rem;
}
div:first-of-type {
flex: 1
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SelectList from '@/components/ui/selectList';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
import sounds from '@/libs/sounds';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
export default {
components: { ToggleSwitch, SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
soundIndex: 0,
previousValue: '',
// using the user.preferences didn't update the select-list values from off state
themeSelected: '',
};
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
availableAudioThemes () {
return this.content.audioThemes;
},
currentAudioTheme () {
return this.user.preferences.sound;
},
isDisabled () {
return this.currentAudioTheme === 'off';
},
},
mounted () {
this.previousValue = this.currentAudioTheme;
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.changeAudioThemeTemporary(this.previousValue);
},
changeAudioThemeTemporary ($event) {
this.user.preferences.sound = $event;
this.themeSelected = $event;
this.soundIndex = 0;
},
changeAudioThemeAndClose () {
this.setUserPreference('sound');
this.previousValue = this.user.preferences.sound;
this.closeModal();
},
playAudio () {
this.$root.$emit('playSound', sounds[this.soundIndex]);
this.soundIndex = (this.soundIndex + 1) % sounds.length;
},
toggleAudioThemeOff (enabled) {
if (enabled) {
const [audioTheme] = this.availableAudioThemes;
this.changeAudioThemeTemporary(audioTheme);
} else {
this.changeAudioThemeTemporary('off');
}
this.modalValuesChanged();
},
},
};
</script>

View file

@ -0,0 +1,252 @@
<template>
<fragment v-if="allowedToChangeClass">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("changeClassSetting") }}
</td>
<td class="settings-value">
<class-icon-label
:selected-class="selectedClass"
:class-disabled="classDisabled"
/>
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="showRealModalOrInline()"
>
{{ $t(classDisabled ? 'chooseClassSetting' : 'edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("changeClassSetting") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("changeClassDisclaimer") }}</span>
</div>
<div class="content-centered">
<div class="current-class mt-3">
<span class="label">{{ $t('currentClass') }}:</span>
<class-icon-label
:selected-class="selectedClass"
:class-disabled="classDisabled"
/>
</div>
<gem-price
gem-price="3"
icon-size="24"
class="gem-price-spacing"
:with-background="true"
/>
<save-cancel-buttons
primary-button-label="changeClassSetting"
class="mb-2"
:no-padding="true"
:disable-save="!enoughGemsAvailable"
@saveClicked="changeClassAndClose()"
@cancelClicked="requestCloseModal()"
/>
<your-balance
:amount-needed="amountNeeded"
currency-needed="gems"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.label-columns {
display: flex;
&:first-of-type {
margin-bottom: 1rem;
}
div:first-of-type {
flex: 1
}
}
.content-centered {
display: flex;
flex-direction: column;
}
.gem-price-spacing {
margin-top: 1.5rem;
margin-bottom: 1.25rem;
justify-content: center;
}
.class-selection {
display: flex;
gap: 22px;
justify-content: center;
margin-bottom: 1.5rem;
margin-top: 1.5rem;
}
.label {
font-size: 14px;
line-height: 1.71;
text-align: center;
}
.selected-badge {
position: absolute;
bottom: -1rem;
width: 24px;
height: 24px;
padding: 4px;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
background-color: $green-50;
border-radius: 1rem;
color: $white;
}
.current-class {
display: flex;
justify-content: center;
.label {
margin-right: 0.5rem;
font-weight: bold;
color: $gray-50;
}
}
</style>
<script>
import axios from 'axios';
import { mapGetters, mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
import YourBalance from '@/pages/settings/components/yourBalance.vue';
import GemPrice from '@/components/shops/gemPrice.vue';
import checkIcon from '@/assets/svg/check.svg';
import changeClass from '@/../../common/script/ops/changeClass';
import ClassIconLabel from '@/pages/settings/components/classIconLabel.vue';
export default {
components: {
ClassIconLabel,
GemPrice,
YourBalance,
SaveCancelButtons,
},
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
amountNeeded: 3 / 4,
selectedClass: '',
icons: Object.freeze({
check: checkIcon,
}),
};
},
computed: {
...mapGetters({
userGems: 'user:gems',
}),
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
content: 'content',
}),
classList () {
return this.content.classes;
},
allowedToChangeClass () {
return this.user.stats.lvl >= 10;
},
enoughGemsAvailable () {
return this.amountNeeded <= this.userGems;
},
classDisabled () {
return this.user.preferences.disableClasses;
},
},
mounted () {
this.selectedClass = this.user.stats.class;
this.resetControls();
},
methods: {
showRealModalOrInline () {
if (!this.classDisabled) {
this.openModal();
} else {
this.changeClassAndClose();
}
},
async changeClassAndClose () {
if (!this.classDisabled && !window.confirm(this.$t('changeClassConfirmCost'))) {
return;
}
this.$root.$once('bv::hide::modal', () => {
// update the label in the settings list
this.selectedClass = this.user.stats.class;
});
try {
await Promise.all([
// resets the class settings and triggers indirectly the modal of
// src/components/achievemnts/chooseClass - I don't know if we should keep this weird way
changeClass(this.user),
axios.post('/api/v4/user/change-class'),
]);
} catch (e) {
window.alert(e.message); // eslint-disable-line no-alert
}
this.closeModal();
},
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.selectedClass = this.user.stats.class;
},
},
};
</script>

View file

@ -0,0 +1,121 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("dateFormat") }}
</td>
<td class="settings-value">
{{ currentActiveFormat }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("dateFormat") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("dateFormatDisclaimer") }}</span>
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("dateFormat") }}
</div>
<div class="form-group">
<select-list
:items="availableFormats"
:value="selectedFormat"
@select="changeFormat($event)"
/>
</div>
</div>
<save-cancel-buttons
:disable-save="selectedFormat === currentActiveFormat"
@saveClicked="changeFormatAndClose()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
</style>
<script>
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SelectList from '@/components/ui/selectList';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
export default {
components: { SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
selectedFormat: '',
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
};
},
computed: {
...mapState({
user: 'user.data',
}),
currentActiveFormat () {
return this.user.preferences.dateFormat;
},
},
mounted () {
this.resetControls();
},
methods: {
changeFormat (e) {
this.selectedFormat = e;
this.modalValuesChanged();
},
async changeFormatAndClose () {
this.user.preferences.dateFormat = this.selectedFormat;
await this.setUserPreference('dateFormat');
this.closeModal();
},
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.selectedFormat = this.currentActiveFormat;
},
},
};
</script>

View file

@ -0,0 +1,181 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("dayStartAdjustment") }}
</td>
<td class="settings-value">
{{ selectedDayStartLabel(user.preferences.dayStart) }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("dayStartAdjustment") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="$t('customDayStartInfo1')"
>
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("adjustment") }}
</div>
<div class="form-group">
<select-list
:items="dayStartOptions"
:value="newDayStart"
key-prop="value"
active-key-prop="value"
:hide-icon="false"
@select="changeDayStart($event)"
>
<template #item="{ item }">
<span v-if="item === newDayStart || (!item && newDayStart === 0)">
{{ selectedDayStartLabel(newDayStart) }}
</span>
<span v-else>
{{ item?.name }}
</span>
</template>
</select-list>
</div>
</div>
<small
class="timezone-explain"
>
<p v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})"></p>
<p v-html="$t('timezoneInfo')"></p>
</small>
<div class="input-area">
<save-cancel-buttons
:disable-save="newDayStart === user.preferences.dayStart"
@saveClicked="saveDayStart()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.timezone-explain {
font-size: 12px;
line-height: 1.33;
color: $gray-100;
text-align: center;
}
</style>
<script>
import axios from 'axios';
import moment from 'moment/moment';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import SelectList from '@/components/ui/selectList.vue';
import getUtcOffset from '../../../../../common/script/fns/getUtcOffset';
export default {
components: { SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
const dayStartOptions = [];
for (let number = 0; number <= 12; number += 1) {
const meridian = number < 12 ? 'AM' : 'PM';
const hour = number % 12;
const timeWithMeridian = `(${hour || 12}:00 ${meridian})`;
const option = {
value: number,
name: `+${number} hours ${timeWithMeridian}`,
};
if (number === 0) {
option.name = `Default ${timeWithMeridian}`;
}
dayStartOptions.push(option);
}
return {
newDayStart: 0,
dayStartOptions,
};
},
mounted () {
this.newDayStart = this.user.preferences.dayStart;
},
computed: {
...mapState({
user: 'user.data',
}),
timezoneOffsetToUtc () {
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
return `UTC${offsetString}`;
},
dayStart () {
return this.user.preferences.dayStart;
},
},
methods: {
changeDayStart ($event) {
this.newDayStart = $event.value;
},
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v4/user/custom-day-start', {
dayStart: this.newDayStart,
});
this.closeModal();
},
selectedDayStartLabel (dayStartValue) {
if (!this.dayStartOptions) {
return '';
}
return this.dayStartOptions.find(l => l.value === dayStartValue)?.name ?? '';
},
calculateNextCron () {
let nextCron = moment()
.hours(this.newDayStart)
.minutes(0)
.seconds(0)
.milliseconds(0);
const currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
},
},
};
</script>

View file

@ -0,0 +1,153 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("deleteAccount") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title danger"
>
{{ $t("deleteAccount") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="hasPassword
? $t('deleteLocalAccountText')
: $t('deleteSocialAccountText', {magicWord: 'DELETE'})"
>
</div>
<current-password-input
v-if="hasPassword"
:show-forget-password="true"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="passwordValue = $event"
/>
<div
v-else
class="input-area"
>
<div
class="form"
>
<div class="settings-label">
{{ $t("confirm") }}
</div>
<div class="form-group">
<input
v-model="passwordValue"
class="form-control"
type="text"
>
</div>
</div>
</div>
<div
v-once
class="feedback"
v-html="$t('feedback')"
>
</div>
<div
class="input-area"
>
<textarea
id="feedbackTextArea"
v-model="feedback"
:placeholder="$t('feedbackPlaceholder')"
class="form-control"
></textarea>
</div>
<div class="input-area">
<save-cancel-buttons
:disable-save="!enableDelete"
primary-button-color="btn-danger"
primary-button-label="deleteAccount"
@saveClicked="deleteAccount()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.feedback {
color: $gray-50;
}
</style>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import CurrentPasswordInput from '../components/currentPasswordInput.vue';
import { PasswordInputChecksMixin } from '@/mixins/passwordInputChecks';
export default {
components: { CurrentPasswordInput, SaveCancelButtons },
mixins: [InlineSettingMixin, PasswordInputChecksMixin],
data () {
return {
passwordValue: '',
feedback: '',
};
},
computed: {
...mapState({
user: 'user.data',
}),
hasPassword () {
return this.user.auth.local.has_password;
},
enableDelete () {
return this.hasPassword ? Boolean(this.passwordValue) : this.passwordValue === 'DELETE';
},
},
methods: {
async deleteAccount () {
await this.passwordInputCheckMixinTryCall(async () => {
await axios.delete('/api/v4/user', {
data: {
password: this.passwordValue,
feedback: this.feedback,
},
});
localStorage.clear();
window.location.href = '/static/home';
});
},
},
};
</script>

View file

@ -0,0 +1,229 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("displayName") }}
</td>
<td class="settings-value">
{{ user.profile.name }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("displayName") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("changeDisplayNameDisclaimer") }}
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("displayName") }}
</div>
<div
class="form"
name="changeDisplayName"
novalidate="novalidate"
>
<div class="form-group">
<input
id="changeDisplayname"
v-model="temporaryDisplayName"
class="form-control"
type="text"
:placeholder="$t('newDisplayName')"
:class="{'is-invalid input-invalid': displayNameInvalid}"
@keyup="valuesChanged()"
>
<div
v-if="displayNameIssues.length > 0"
class="mb-3"
>
<div
v-for="issue in displayNameIssues"
:key="issue"
class="input-error"
>
{{ issue }}
</div>
</div>
</div>
</div>
<save-cancel-buttons
:disable-save="displayNameCannotSubmit"
@saveClicked="changeDisplayName(temporaryDisplayName)"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group {
position: relative;
background: white;
}
input {
margin-right: 2rem;
}
.input-floating-checkmark {
position: absolute;
background: none !important;
right: 0.5rem;
top: 0.5rem;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.input-group.is-valid {
border-color: $green-10 !important;
}
.input-group:not(.is-valid) {
.check-icon {
display: none;
}
}
.check-icon {
width: 12px;
height: 10px;
color: $green-50;
}
.form-group {
margin-bottom: 1.5rem;
}
</style>
<script>
import axios from 'axios';
import * as validator from 'validator';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
import checkIcon from '@/assets/svg/check.svg';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import NotificationMixins from '@/mixins/notifications';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin, NotificationMixins],
data () {
return {
temporaryDisplayName: '',
inputChanged: false,
displayNameIssues: [],
updates: {
newEmail: '',
password: '',
},
icons: Object.freeze({
checkIcon,
}),
};
},
computed: {
...mapState({
user: 'user.data',
}),
validEmail () {
return validator.isEmail(this.updates.newEmail);
},
allowedToSave () {
return !this.validEmail || this.updates.password.length === 0;
},
displayNameInvalid () {
if (this.temporaryDisplayName.length <= 1) {
return true;
}
return this.displayNameIssues.length !== 0;
},
displayNameCannotSubmit () {
return this.displayNameInvalid || !this.inputChanged;
},
},
watch: {
temporaryDisplayName: {
handler () {
this.validateDisplayName(this.temporaryDisplayName);
},
deep: true,
},
},
mounted () {
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.temporaryDisplayName = this.user.profile.name;
},
async changeDisplayName (newName) {
await axios.put('/api/v4/user/', { 'profile.name': newName });
this.text(this.$t('displayNameSuccess'));
this.user.profile.name = newName;
this.temporaryDisplayName = newName;
this.closeModal();
},
validateDisplayName: debounce(async function checkName (displayName) {
if (displayName.length <= 1 || displayName === this.user.profile.name) {
this.displayNameIssues = [];
return;
}
const res = await this.$store.dispatch('auth:verifyDisplayName', {
displayName,
});
if (res.issues !== undefined) {
this.displayNameIssues = res.issues;
} else {
this.displayNameIssues = [];
}
}, 500),
valuesChanged () {
this.inputChanged = true;
this.modalValuesChanged();
},
},
};
</script>

View file

@ -0,0 +1,291 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("fixValues") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td
colspan="3"
novalidate="novalidate"
>
<div
v-once
class="dialog-title"
>
{{ $t("fixValues") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span v-html="$t('fixValuesText1')"></span>
<br>
<br>
<span v-html="$t('fixValuesText2')"></span>
</div>
<div class="content-centered">
<div class="input-rows row">
<div
v-for="input in inputList"
:key="input.property"
class="col-4"
>
<div class="fix-value-group mt-3">
<span class="fix-label">
{{ $t(input.translationKey) }}
</span>
<div class="input-group">
<div class="input-group-prepend positive-addon input-group-icon">
<div
v-once
class="svg-icon icon-16"
:class="{[input.translationKey]: true}"
v-html="input.icon"
></div>
</div>
<input
v-model="restoreValues[input.property]"
class="form-control"
type="number"
min="0"
required="required"
@keydown="markAsChanged(input, $event)"
>
</div>
</div>
</div>
</div>
</div>
<save-cancel-buttons
:disable-save="!mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues"
class="mt-4"
@saveClicked="save()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group {
position: relative;
background: white;
}
.input-rows {
width: calc(600px + 1.5rem);
}
.content-centered {
display: flex;
flex-direction: column;
align-items: center;
}
.fix-label {
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
.svg-icon.icon-16 {
width: 16px !important;
height: 16px !important;
display: flex;
}
input[type="number"] {
-moz-appearance: textfield !important;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
.svg-icon.level {
color: $gray-200;
:global svg path {
fill: currentColor;
}
}
</style>
<script>
// import clone from 'lodash/clone';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import healthIcon from '@/assets/svg/health.svg';
import experienceIcon from '@/assets/svg/experience.svg';
import manaIcon from '@/assets/svg/mana.svg';
import svgGold from '@/assets/svg/gold.svg';
import level from '@/assets/svg/level.svg';
import streakIcon from '@/assets/svg/streak.svg';
import { mapState } from '@/libs/store';
import { MAX_LEVEL_HARD_CAP } from '../../../../../common/script/constants';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
return {
restoreValues: {
hp: 0,
mp: 0,
gp: 0,
exp: 0,
lvl: 0,
streak: 0,
},
icons: Object.freeze({
health: healthIcon,
experience: experienceIcon,
mana: manaIcon,
gold: svgGold,
level,
streak: streakIcon,
}),
inputList: Object.freeze([
{
translationKey: 'health',
icon: healthIcon,
property: 'hp',
},
{
translationKey: 'experience',
icon: experienceIcon,
property: 'exp',
}, {
translationKey: 'mana',
icon: manaIcon,
property: 'mp',
}, {
translationKey: 'gold',
icon: svgGold,
property: 'gp',
},
{
translationKey: 'level',
icon: level,
property: 'lvl',
},
{
translationKey: 'fix21Streaks',
icon: streakIcon,
property: 'streak',
},
]),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.resetControls();
},
methods: {
resetControls () {
const {
hp, mp, gp, exp, lvl,
} = this.user.stats;
this.restoreValues = {
hp, mp, gp, exp, lvl, streak: this.user.achievements.streak,
};
},
close () {
this.validateInputs();
},
markAsChanged (inputType, keyupEvent) {
this.restoreValues[inputType.property] = keyupEvent.target.value;
this.modalValuesChanged();
},
save () {
if (!this.validateInputs()) {
return;
}
if (this.restoreValues.lvl > MAX_LEVEL_HARD_CAP) {
this.restoreValues.lvl = MAX_LEVEL_HARD_CAP;
}
const userChangedLevel = this.restoreValues.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) {
this.restoreValues.exp = 0;
}
const settings = {
'stats.hp': Number(this.restoreValues.hp),
'stats.exp': Number(this.restoreValues.exp),
'stats.gp': Number(this.restoreValues.gp),
'stats.lvl': Number(this.restoreValues.lvl),
'stats.mp': Number(this.restoreValues.mp),
'achievements.streak': Number(this.restoreValues.streak),
};
this.$store.dispatch('user:set', settings);
this.wasChanged = false;
this.closeModal();
},
validateInputs () {
const canRestore = ['hp', 'exp', 'gp', 'mp'];
let valid = true;
for (const stat of canRestore) {
if (this.restoreValues[stat] === ''
|| this.restoreValues[stat] < 0
) {
this.restoreValues[stat] = this.user.stats[stat];
valid = false;
}
}
const inputLevel = Number(this.restoreValues.lvl);
if (this.restoreValues.lvl === ''
|| !Number.isInteger(inputLevel)
|| inputLevel < 1) {
this.restoreValues.lvl = this.user.stats.lvl;
valid = false;
}
const inputStreak = Number(this.restoreValues.streak);
if (this.restoreValues.streak === ''
|| !Number.isInteger(inputStreak)
|| inputStreak < 0) {
this.restoreValues.streak = this.user.achievements.streak;
valid = false;
}
return valid;
},
},
};
</script>

View file

@ -0,0 +1,62 @@
<template>
<fragment>
<tr>
<td class="settings-label">
{{ $t("showHeader") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<toggle-switch
v-model="user.preferences.showHeader"
@change="setUserPreference('showHeader')"
/>
</td>
</tr>
<tr>
<td class="settings-label">
{{ $t("stickyHeader") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<toggle-switch
v-model="user.preferences.stickyHeader"
@change="setUserPreference('stickyHeader')"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
::v-deep {
.toggle-switch-outer {
display: inline-block;
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
export default {
components: { ToggleSwitch },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
},
};
</script>

View file

@ -0,0 +1,145 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("language") }}
</td>
<td class="settings-value">
{{ currentLanguageLabel }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("language") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
<span>{{ $t("americanEnglishGovern") }} </span>
<span v-html="$t('helpWithTranslation')"></span>
</div>
<div class="input-area">
<div class="settings-label">
{{ $t("siteLanguage") }}
</div>
<div class="form-group">
<select-list
:items="availableLanguages"
:value="selectedLanguage"
key-prop="code"
active-key-prop="code"
@select="changeLanguage($event)"
>
<template #item="{ item }">
<span v-if="item === selectedLanguage">
{{ selectedLanguageLabel(selectedLanguage) }}
</span>
<span v-else>
{{ item.name }}
</span>
</template>
</select-list>
</div>
</div>
<save-cancel-buttons
:disable-save="selectedLanguage === currentActiveLanguage"
@saveClicked="changeLanguageAndClose()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
input {
margin-right: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
</style>
<script>
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SelectList from '@/components/ui/selectList';
import { GenericUserPreferencesMixin } from '../components/genericUserPreferencesMixin';
export default {
components: { SelectList, SaveCancelButtons },
mixins: [InlineSettingMixin, GenericUserPreferencesMixin],
data () {
return {
selectedLanguage: '',
};
},
computed: {
...mapState({
user: 'user.data',
availableLanguages: 'i18n.availableLanguages',
}),
currentActiveLanguage () {
return this.user.preferences.language;
},
currentLanguageLabel () {
return this.selectedLanguageLabel(this.selectedLanguage);
},
},
mounted () {
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.selectedLanguage = this.currentActiveLanguage;
},
changeLanguage (e) {
const newLang = e.code;
this.selectedLanguage = newLang;
this.modalValuesChanged();
},
selectedLanguageLabel (languageKey) {
if (!this.availableLanguages) {
return '';
}
return this.availableLanguages.find(l => l.code === languageKey)?.name ?? '';
},
async changeLanguageAndClose () {
this.user.preferences.language = this.selectedLanguage;
await this.setUserPreference('language');
setTimeout(() => window.location.reload(true));
},
},
};
</script>

View file

@ -0,0 +1,185 @@
<template>
<fragment>
<tr
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
>
<td class="settings-label">
<div class="network-icon-with-label">
<span
:class="'svg-icon icon-16 social-icon ' + network.key"
v-html="icons[network.key]"
></span>
<span class="ml-75"> {{ network.name }}</span>
</div>
</td>
<td class="settings-value">
<div
v-if="isConnected(network.key)"
class="connected-pill"
>
{{ $t('connected') }}
</div>
</td>
<td class="settings-button">
<a
v-if="allowedToConnect(network.key)"
class="edit-link"
@click.prevent="socialAuth(network.key, user)"
>
{{ $t('connect') }}
</a>
<a
v-if="allowedToRemove(network.key)"
class="remove-link"
@click.prevent="deleteSocialAuth(network)"
>
{{ $t('remove') }}
</a>
</td>
</tr>
</fragment>
</template>
<script>
import axios from 'axios';
import hello from 'hellojs';
import { buildAppleAuthUrl } from '@/libs/auth';
import { mapState } from '@/libs/store';
import { SUPPORTED_SOCIAL_NETWORKS } from '../../../../../common/script/constants';
import googleIcon from '@/assets/svg/google.svg';
import appleIcon from '@/assets/svg/apple_black.svg';
export default {
name: 'LoginMethods',
data () {
return {
SOCIAL_AUTH_NETWORKS: [],
// Made available by the server as a script
localAuth: {
password: '',
confirmPassword: '',
},
icons: Object.freeze({
google: googleIcon,
apple: appleIcon,
}),
};
},
computed: {
...mapState({
user: 'user.data',
content: 'content',
}),
},
mounted () {
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
});
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
}, {
redirect_uri: '', // eslint-disable-line
});
const focusID = this.$route.query.focus;
if (focusID !== undefined && focusID !== null) {
this.$nextTick(() => {
const element = document.getElementById(focusID);
if (element !== undefined && element !== null) {
element.focus();
}
});
}
},
methods: {
async deleteSocialAuth (network) {
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
this.user.auth[network.key] = {};
this.text(this.$t('detachedSocial', { network: network.name }));
},
async socialAuth (network) {
if (network === 'apple') {
window.location.href = buildAppleAuthUrl();
} else {
const auth = await hello(network).login({ scope: 'email' });
await this.$store.dispatch('auth:socialAuth', {
auth,
});
window.location.href = '/';
}
},
hasBackupAuthOption (networkKeyToCheck) {
if (this.user.auth.local.username && this.user.auth.local.has_password) {
return true;
}
return this.SOCIAL_AUTH_NETWORKS.find(network => {
if (network.key !== networkKeyToCheck) {
if (this.user.auth[network.key]) {
return !!this.user.auth[network.key].id;
}
}
return false;
});
},
isConnected (networkKeyToCheck) {
return !!this.user.auth[networkKeyToCheck].id;
},
allowedToConnect (networkKeyToCheck) {
if (networkKeyToCheck === 'facebook') {
return false; // is still needed? the list of networks doesn't have facebook
}
const isConnected = this.isConnected(networkKeyToCheck);
return !isConnected;
},
allowedToRemove (networkKeyToCheck) {
const isConnected = this.isConnected(networkKeyToCheck);
return isConnected && this.hasBackupAuthOption(networkKeyToCheck);
},
},
};
</script>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.icon-16 ::v-deep svg {
height: 16px;
width: 16px;
}
.network-icon-with-label {
display: flex;
align-items: center;
flex-direction: row;
span:not(.svg-icon) {
flex: 1;
}
}
.connected-pill {
display: inline-block;
padding: 4px 12px;
border-radius: 100px;
background-color: $green-50;
font-size: 12px;
line-height: 1.33;
color: $white;
}
.social-icon.apple {
margin-bottom: -2px !important;
}
</style>

View file

@ -0,0 +1,189 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("password") }}
</td>
<td class="settings-value"></td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t(hasPassword ? 'edit' : 'add') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td
colspan="3"
novalidate="novalidate"
>
<div
v-once
class="dialog-title"
>
{{ $t("password") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("changePasswordDisclaimer") }}
</div>
<current-password-input
v-if="hasPassword"
:show-forget-password="true"
custom-label="currentPass"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="passwordUpdates.password = $event"
/>
<current-password-input
custom-label="newPass"
:is-valid="mixinData.newPasswordIssues.length === 0"
:invalid-issues="mixinData.newPasswordIssues"
@passwordValue="passwordUpdates.newPassword = $event"
/>
<current-password-input
custom-label="confirmPass"
:is-valid="mixinData.confirmPasswordIssues.length === 0"
:invalid-issues="mixinData.confirmPasswordIssues"
@passwordValue="passwordUpdates.confirmPassword = $event"
/>
<save-cancel-buttons
:disable-save="inputsInvalid"
@saveClicked="hasPassword ? changePassword() : addLocalAuth()"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.input-group {
position: relative;
background: white;
}
input {
margin-right: 2rem;
}
.input-floating-checkmark {
position: absolute;
background: none !important;
right: 0.5rem;
top: 0.5rem;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
}
.input-group.is-valid {
border-color: $green-10 !important;
}
.input-group:not(.is-valid) {
.check-icon {
display: none;
}
}
.check-icon {
width: 12px;
height: 10px;
color: $green-50;
}
</style>
<script>
import axios from 'axios';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import CurrentPasswordInput from '../components/currentPasswordInput.vue';
import { mapState } from '@/libs/store';
import { PasswordInputChecksMixin } from '@/mixins/passwordInputChecks';
export default {
components: { CurrentPasswordInput, SaveCancelButtons },
mixins: [InlineSettingMixin, PasswordInputChecksMixin],
data () {
return {
passwordUpdates: {
password: '',
newPassword: '',
confirmPassword: '',
},
};
},
computed: {
...mapState({
user: 'user.data',
}),
hasPassword () {
return this.user.auth.local.has_password;
},
inputsInvalid () {
if (this.hasPassword && !this.passwordUpdates.password) {
return true;
}
return this.passwordUpdates.newPassword !== this.passwordUpdates.confirmPassword;
},
},
methods: {
async changePassword () {
await this.passwordInputCheckMixinTryCall(async () => {
const localAuthData = {
password: this.passwordUpdates.password,
newPassword: this.passwordUpdates.newPassword,
confirmPassword: this.passwordUpdates.confirmPassword,
};
await axios.put('/api/v4/user/auth/update-password', localAuthData);
this.passwordUpdates = {};
this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('passwordSuccess'),
type: 'success',
timeout: true,
});
});
},
async addLocalAuth () {
await this.passwordInputCheckMixinTryCall(async () => {
const localAuthData = {
password: this.passwordUpdates.newPassword,
confirmPassword: this.passwordUpdates.confirmPassword,
email: this.user.auth.local.email,
username: this.user.auth.local.username,
};
await axios.post('/api/v4/user/auth/local/register', localAuthData);
window.location.reload();
});
},
},
};
</script>

View file

@ -0,0 +1,135 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("resetAccount") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
v-if="!!user?.auth?.local?.username"
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title danger"
>
{{ $t("resetAccount") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="$t('resetText1')"
>
</div>
<div class="split-lists my-3 ">
<ul>
<li
v-once
>
{{ $t('resetDetail1') }}
</li>
<li v-once>
{{ $t('resetDetail3') }}
</li>
</ul>
<ul>
<li v-once>
{{ $t('resetDetail2') }}
</li>
<li v-once>
{{ $t('resetDetail4') }}
</li>
</ul>
</div>
<div
v-once
v-html="$t('resetText2')"
>
</div>
<div class="input-area">
<current-password-input
:show-forget-password="true"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="passwordValue = $event"
/>
<save-cancel-buttons
:disable-save="passwordValue === ''"
primary-button-color="btn-danger"
primary-button-label="resetAccount"
@saveClicked="reset()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.split-lists {
display: flex;
flex-direction: row;
color: $gray-50;
ul {
flex: 0 0 50%;
}
}
</style>
<script>
import axios from 'axios';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import CurrentPasswordInput from '../components/currentPasswordInput.vue';
import { PasswordInputChecksMixin } from '@/mixins/passwordInputChecks';
export default {
components: { CurrentPasswordInput, SaveCancelButtons },
mixins: [InlineSettingMixin, PasswordInputChecksMixin],
data () {
return {
passwordValue: '',
};
},
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
async reset () {
await this.passwordInputCheckMixinTryCall(async () => {
await axios.post('/api/v4/user/reset', {
password: this.passwordValue,
});
this.$router.push('/');
setTimeout(() => window.location.reload(true), 100);
});
},
},
};
</script>

View file

@ -0,0 +1,95 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("pauseDailies") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("pauseDailies") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="$t('sleepDescription')"
>
</div>
<ul>
<li v-once>
{{ $t('sleepBullet1') }}
</li>
<li v-once>
{{ $t('sleepBullet2') }}
</li>
<li v-once>
{{ $t('sleepBullet3') }}
</li>
</ul>
<div class="input-area">
<save-cancel-buttons
:primary-button-label="user.preferences.sleep ? 'unpauseDailies' : 'pauseDailies'"
@saveClicked="toggleSleep(); requestCloseModal();"
@cancelClicked="requestCloseModal();"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.feedback {
color: $gray-50;
}
</style>
<script>
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
return {};
},
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
toggleSleep () {
this.$store.dispatch('user:sleep');
},
},
};
</script>

View file

@ -0,0 +1,136 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("email") }}
</td>
<td class="settings-value">
{{ user?.auth?.local?.email }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('edit') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("email") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("changeEmailDisclaimer") }}
</div>
<div class="input-area">
<validated-text-input
v-model="updates.newEmail"
settings-label="email"
:is-valid="validEmail"
@update:value="modalValuesChanged"
@blur="restoreEmptyEmail()"
/>
<current-password-input
:show-forget-password="true"
:is-valid="mixinData.currentPasswordIssues.length === 0"
:invalid-issues="mixinData.currentPasswordIssues"
@passwordValue="updates.password = $event"
/>
<save-cancel-buttons
:disable-save="disallowedToSave"
@saveClicked="changeEmail()"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
</style>
<script>
import axios from 'axios';
import * as validator from 'validator';
import { mapState } from '@/libs/store';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import CurrentPasswordInput from '../components/currentPasswordInput.vue';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
import NotificationMixins from '@/mixins/notifications';
import { PasswordInputChecksMixin } from '@/mixins/passwordInputChecks';
export default {
components: { ValidatedTextInput, CurrentPasswordInput, SaveCancelButtons },
mixins: [InlineSettingMixin, NotificationMixins, PasswordInputChecksMixin],
data () {
return {
updates: {
newEmail: '',
password: '',
},
previousEmail: '',
};
},
computed: {
...mapState({
user: 'user.data',
}),
emailChanged () {
return this.previousEmail !== this.updates.newEmail;
},
validEmail () {
return validator.isEmail(this.updates.newEmail);
},
disallowedToSave () {
return !this.emailChanged
|| !this.validEmail
|| this.updates.password.length === 0;
},
},
mounted () {
this.restoreEmptyEmail();
},
methods: {
resetControls () {
this.restoreEmail();
},
restoreEmptyEmail () {
if (this.updates.newEmail.length < 1) {
this.restoreEmail();
}
},
restoreEmail () {
this.updates.newEmail = this.user.auth.local.email;
this.previousEmail = this.user.auth.local.email;
},
async changeEmail () {
await this.passwordInputCheckMixinTryCall(async () => {
await axios.put('/api/v4/user/auth/update-email', this.updates);
this.user.auth.local.email = this.updates.newEmail;
this.text(this.$t('emailSuccess'));
this.closeModal();
});
},
},
};
</script>

View file

@ -0,0 +1,169 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("username") }}
</td>
<td class="settings-value">
{{ user?.auth?.local?.username }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t(user?.auth?.local?.username ? 'edit' : 'add') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("username") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("changeUsernameDisclaimer") }}
</div>
<div class="input-area">
<validated-text-input
v-model="inputValue"
settings-label="username"
:is-valid="usernameValid"
:invalid-issues="usernameIssues"
@update:value="valuesChanged()"
@blur="restoreEmptyUsername()"
/>
<save-cancel-buttons
:disable-save="usernameCannotSubmit"
@saveClicked="changeUser('username', cleanedInputValue)"
@cancelClicked="requestCloseModal()"
/>
</div>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
</style>
<script>
import axios from 'axios';
import debounce from 'lodash/debounce';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '../components/saveCancelButtons.vue';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
import { NotificationMixins } from '@/mixins/notifications';
// TODO extract usernameIssues/checks to a mixin to share between this and the authForm
export default {
components: { ValidatedTextInput, SaveCancelButtons },
mixins: [InlineSettingMixin, NotificationMixins],
data () {
return {
inputValue: '',
inputChanged: false,
usernameIssues: [],
};
},
computed: {
...mapState({
user: 'user.data',
}),
cleanedInputValue () {
return this.inputValue.startsWith('@')
// remove the @ from the value, only if its starting with
? this.inputValue.replace('@', '')
// not removing it creates an error that is displayed
: this.inputValue;
},
usernameValid () {
if (this.cleanedInputValue.length <= 1) {
return false;
}
return this.usernameIssues.length === 0;
},
usernameCannotSubmit () {
if (this.cleanedInputValue.length <= 1) {
return true;
}
return !this.usernameValid || !this.inputChanged;
},
},
watch: {
inputValue () {
this.validateUsername(this.cleanedInputValue);
},
},
mounted () {
this.resetControls();
},
methods: {
/**
* is a callback from the {InlineSettingMixin}
* do not remove
*/
resetControls () {
this.inputValue = `@${this.user.auth.local.username}`;
},
restoreEmptyUsername () {
if (this.inputValue.length < 1) {
this.resetControls();
}
},
async changeUser (attribute, newUsername) {
await axios.put(`/api/v4/user/auth/update-${attribute}`, {
username: newUsername,
});
this.user.auth.local.username = newUsername;
this.user.flags.verifiedUsername = true;
this.text(this.$t('userNameSuccess'));
this.closeModal();
},
valuesChanged () {
this.inputChanged = true;
this.modalValuesChanged();
},
validateUsername: debounce(async function checkName (username) {
if (username.length <= 1 || username === this.user.auth.local.username) {
this.usernameIssues = [];
return;
}
const res = await this.$store.dispatch('auth:verifyUsername', {
username,
});
if (res.issues !== undefined) {
this.usernameIssues = res.issues;
} else {
this.usernameIssues = [];
}
}, 500),
},
};
</script>

View file

@ -0,0 +1,71 @@
<template>
<div class="row standard-page">
<div class="col-12">
<h1
v-once
class="page-header"
>
{{ $t('siteData') }}
</h1>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('user') }}
</h2>
<table class="table">
<user-id-row />
<user-data-row />
<tr>
<td colspan="3">
</td>
</tr>
</table>
</div>
<div class="col-12">
<h2 v-once>
{{ $t('api') }}
</h2>
<table class="table">
<api-row />
<developer-mode-row />
<tr>
<td colspan="3">
</td>
</tr>
</table>
<webhooks-row />
</div>
</div>
</template>
<script>
import UserIdRow from '@/pages/settings/siteDataRows/userIdRow.vue';
import UserDataRow from '@/pages/settings/siteDataRows/userDataRow.vue';
import ApiRow from '@/pages/settings/siteDataRows/apiRow.vue';
import WebhooksRow from '@/pages/settings/siteDataRows/webhooksRow.vue';
import DeveloperModeRow from '@/pages/settings/siteDataRows/developerModeRow.vue';
export default {
components: {
DeveloperModeRow,
WebhooksRow,
ApiRow,
UserDataRow,
UserIdRow,
},
mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('settings'),
subSection: this.$t('siteData'),
});
},
};
</script>
<style scoped>
</style>

View file

@ -0,0 +1,131 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("APITokenTitle") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("APITokenTitle") }}
</div>
<div
v-once
class="dialog-disclaimer"
v-html="$t('APITokenDisclaimer')"
>
</div>
<div class="d-flex justify-content-center api-key-input">
<locked-input
:label="$t('APITokenTitle')"
:value="apiToken"
:notification-text="$t('APICopied')"
/>
</div>
<save-cancel-buttons
:hide-save="true"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.api-key-input {
margin-top: 20px;
margin-bottom: 0;
td {
border: 0;
padding: 0 !important;
&:first-of-type {
text-align: end;
vertical-align: middle;
padding-right: 1rem !important;
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
}
::v-deep {
.dropdown-menu {
min-width: 0;
}
.form-group {
margin-bottom: 0;
}
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '@/pages/settings/components/saveCancelButtons.vue';
import LockedInput from '@/pages/settings/components/lockedInput.vue';
export default {
components: { LockedInput, SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
return {};
},
mounted () {
window.addEventListener('message', this.receiveMessage, false);
},
destroy () {
window.removeEventListener('message', this.receiveMessage);
},
computed: {
...mapState({
user: 'user.data',
credentials: 'credentials',
}),
apiToken () {
return this.credentials.API_TOKEN;
},
},
methods: {
receiveMessage (eventFrom) {
if (eventFrom.origin !== 'https://www.spritely.app') {
return;
}
const creds = {
userId: this.user._id,
apiToken: this.credentials.API_TOKEN,
};
eventFrom.source.postMessage(creds, eventFrom.origin);
},
},
};
</script>

View file

@ -0,0 +1,56 @@
<template>
<tr>
<td class="settings-label">
<div class="d-flex align-items-center">
{{ $t("developerMode") }}
<information-icon
tooltip-id="developerMode"
:tooltip="$t('developerModeTooltip')"
/>
</div>
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<toggle-switch
v-model="user.preferences.developerMode"
@change="setUserPreference('developerMode')"
/>
</td>
</tr>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
::v-deep {
.toggle-switch-outer {
display: inline-block;
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
import informationIcon from '@/assets/svg/information.svg';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default {
components: { InformationIcon, ToggleSwitch },
mixins: [GenericUserPreferencesMixin],
data () {
return {
icons: Object.freeze({
information: informationIcon,
}),
};
},
computed: {
...mapState({
user: 'user.data',
}),
},
};
</script>

View file

@ -0,0 +1,139 @@
<template>
<fragment>
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
<td class="settings-label">
{{ $t("yourUserData") }}
</td>
<td class="settings-value">
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="openModal()"
>
{{ $t('learnMore') }}
</a>
</td>
</tr>
<tr
v-if="mixinData.inlineSettingMixin.modalVisible"
class="expanded"
>
<td colspan="3">
<div
v-once
class="dialog-title"
>
{{ $t("yourUserData") }}
</div>
<div
v-once
class="dialog-disclaimer"
>
{{ $t("yourUserDataDisclaimer") }}
</div>
<div class="d-flex justify-content-center data-download-selection">
<table v-once>
<tr>
<td>{{ $t('taskHistory') }}</td>
<td>
<a
href="/export/history.csv"
class="btn btn-secondary"
>
{{ $t('downloadCSV') }}
</a>
</td>
</tr>
<tr>
<td>{{ $t('userData') }}</td>
<td>
<b-dropdown
:text="$t('downloadAs')"
right="right"
>
<b-dropdown-item
href="/export/userdata.xml"
>
{{ $t('xml') }}
</b-dropdown-item>
<b-dropdown-item
href="/export/userdata.json"
>
{{ $t('json') }}
</b-dropdown-item>
</b-dropdown>
</td>
</tr>
</table>
</div>
<save-cancel-buttons
:hide-save="true"
@cancelClicked="requestCloseModal()"
/>
</td>
</tr>
</fragment>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.data-download-selection {
margin-top: 20px;
margin-bottom: 0;
td {
border: 0 !important;
padding-bottom: 0 !important;
&:first-of-type {
text-align: end;
vertical-align: middle;
padding-right: 0.5rem !important;
font-weight: bold;
line-height: 1.71;
color: $gray-50;
}
}
tr:first-of-type {
td {
padding-bottom: 0.5rem !important;
}
}
::v-deep {
.dropdown-menu {
min-width: 0;
}
}
}
</style>
<script>
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import SaveCancelButtons from '@/pages/settings/components/saveCancelButtons.vue';
export default {
components: { SaveCancelButtons },
mixins: [InlineSettingMixin],
data () {
return {};
},
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {},
};
</script>

View file

@ -0,0 +1,58 @@
<template>
<tr>
<td class="settings-label">
<div
v-once
class="d-flex align-items-center"
>
{{ $t("userId") }} <information-icon
tooltip-id="userId"
:tooltip="$t('userIdTooltip')"
/>
</div>
</td>
<td
v-once
class="settings-value"
>
{{ user.id }}
</td>
<td class="settings-button">
<a
class="edit-link"
@click.prevent="copyUserId()"
>
{{ $t('copy') }}
</a>
</td>
</tr>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
</style>
<script>
import { mapState } from '@/libs/store';
import copyToClipboard from '@/mixins/copyToClipboard';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default {
components: { InformationIcon },
mixins: [copyToClipboard],
computed: {
...mapState({
user: 'user.data',
}),
},
methods: {
copyUserId () {
this.mixinCopyToClipboard(
this.user.id,
this.$t('useridCopied'),
);
},
},
};
</script>

View file

@ -0,0 +1,323 @@
<template>
<div>
<h2
v-once
>
{{ $t("webhooks") }}
</h2>
<div
v-once
class="webhooks-info mb-3"
v-html="$t('webhooksInfo')"
>
</div>
<div
class="d-flex justify-content-center webhooks-list"
:class="{'webhooks-exists': Boolean(webhooks.length)}"
>
<table class="table table-striped">
<tr v-if="webhooks.length">
<th>{{ $t('webhookURL') }}</th>
<th>{{ $t('enabled') }}</th>
<th></th>
</tr>
<tr
v-for="(webhook, index) in webhooks"
:key="webhook.id"
>
<td style="width: 588px">
<div class="d-flex align-items-center">
<div style="width: 440px">
<validated-text-input
v-model="webhook.url"
:placeholder="$t('webhookURL')"
:is-valid="isValidUrl(webhook.url)"
:readonly="!unsaved.includes(index)"
/>
</div>
<template v-if="unsaved.includes(index)">
<button
class="btn btn-primary ml-2"
:disabled="!isValidUrl(webhook.url)"
@click="saveWebhook(webhook, index)"
>
Save
</button>
<a
class="edit-link ml-3"
@click.prevent="cancelWebhookChanges(webhook, index)"
>
{{ $t('cancel') }}
</a>
</template>
</div>
</td>
<td style="vertical-align: middle;">
<toggle-switch
v-if="!unsaved.includes(index)"
v-model="webhook.enabled"
@change="updateWebhookEnabled(webhook, index)"
/>
</td>
<td class="menu-column">
<b-dropdown
v-if="!unsaved.includes(index)"
right="right"
toggle-class="with-icon"
class="ml-2"
:no-caret="true"
>
<template #button-content>
<span
v-once
class="svg-icon inline menuIcon"
v-html="icons.menuIcon"
>
</span>
</template>
<b-dropdown-item
class="selectListItem"
@click="editWebhook(webhook, index)"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.editIcon"
></span>
<span v-once>
{{ $t('edit') }}
</span>
</span>
</b-dropdown-item>
<b-dropdown-item
class="selectListItem custom-hover--delete"
@click="deleteWebhook(webhook, index)"
>
<span class="with-icon">
<span
v-once
class="svg-icon icon-16 color"
v-html="icons.deleteIcon"
></span>
<span v-once>
{{ $t('delete') }}
</span>
</span>
</b-dropdown-item>
</b-dropdown>
</td>
</tr>
<tr>
<td
colspan="3"
:class="{'webhooks-empty': !Boolean(webhooks.length)}"
>
<button
class="btn btn-secondary d-flex align-items-center new-webhook-btn"
:class="{'webhooks-exists': Boolean(webhooks.length)}"
tabindex="0"
@click="newUnsavedWebhook()"
>
<div
class="svg-icon icon-10 color"
v-html="icons.positive"
></div>
<div class="ml-75 mr-1">
{{ $t('addWebhook') }}
</div>
</button>
</td>
</tr>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.webhooks-info {
line-height: 1.71;
color: $gray-50;
}
.svg-icon.icon-10 {
color: $green-10;
}
.menuIcon {
width: 4px;
height: 1rem;
object-fit: contain;
}
.custom-hover--delete {
--hover-color: #{$maroon-50};
--hover-background: #ffb6b83F;
}
.webhooks-list {
margin-bottom: 0.5rem;
tr:first-of-type {
th {
padding: 0.25rem;
border-top: 0;
}
}
td {
padding: 0.5rem !important;
&:first-of-type {
text-align: end;
vertical-align: middle;
padding-right: 1rem !important;
line-height: 1.71;
color: $gray-50;
}
&:not(:first-of-type) {
padding-right: 0 !important;
padding-left: 0 !important;
}
}
}
td.webhooks-empty {
border-top-color: transparent;
}
td.menu-column {
width: 2rem;
padding-left: 0 !important;
padding-right: 0 !important;
}
.new-webhook-btn:not(.webhooks-exists) {
margin: 0 auto;
}
table {
margin-bottom: 0 !important;
}
</style>
<script>
import * as validator from 'validator';
import { mapState } from '@/libs/store';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import uuid from '../../../../../common/script/libs/uuid';
import positiveIcon from '@/assets/svg/positive.svg';
import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import menuIcon from '@/assets/svg/menu.svg';
import deleteIcon from '@/assets/svg/delete.svg';
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
import editIcon from '@/assets/svg/edit.svg';
export default {
components: { ValidatedTextInput, ToggleSwitch },
mixins: [InlineSettingMixin],
data () {
return {
icons: Object.freeze({
positive: positiveIcon,
menuIcon,
deleteIcon,
editIcon,
}),
webhooks: [], // view copy of state
unsaved: [],
};
},
mounted () {
this.setWebhooksViewCopy();
},
computed: {
...mapState({
user: 'user.data',
credentials: 'credentials',
}),
},
methods: {
isValidUrl (url) {
return validator.isURL(url, {
require_tld: true,
require_protocol: true,
protocols: ['http', 'https'],
});
},
async newUnsavedWebhook () {
const webhookInfo = {
id: uuid(),
type: 'taskActivity',
options: {
created: false,
updated: false,
deleted: false,
scored: true,
},
url: '',
enabled: true,
};
this.unsaved.push(
this.webhooks.push(webhookInfo) - 1,
);
},
cancelWebhookChanges (webhook, index) {
if (this.unsaved.includes(index)) {
this.unsaved = this.unsaved.filter(i => i !== index);
}
if (this.user.webhooks[index]) {
this.webhooks[index] = this.user.webhooks[index];
} else {
this.webhooks.splice(index, 1);
}
},
async saveWebhook (webhook, index) {
if (!this.isValidUrl(webhook.url)) {
return;
}
const webhookId = webhook.id;
if (this.user.webhooks.every(w => w.id !== webhookId)) {
const createdWebhook = await this.$store.dispatch('user:addWebhook', { webhook });
this.user.webhooks[index] = createdWebhook;
} else {
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
}
this.cancelWebhookChanges(webhook, index);
},
async updateWebhookEnabled (webhook, index) {
if (this.unsaved.includes(index)) {
return;
}
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
this.user.webhooks[index] = updatedWebhook;
},
async editWebhook (webhook, index) {
this.unsaved.push(index);
},
async deleteWebhook (webhook, index) {
await this.$store.dispatch('user:deleteWebhook', { webhook });
this.user.webhooks.splice(index, 1);
this.setWebhooksViewCopy();
},
setWebhooksViewCopy () {
this.webhooks = [...this.user.webhooks];
},
},
};
</script>

View file

@ -88,6 +88,8 @@ const router = new VueRouter({
{ name: 'logout', path: '/logout', component: Logout },
{
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
}, {
name: 'forgotPassword', path: '/forgot-password', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{ name: 'tasks', path: '/', component: UserTasks },
{

View file

@ -1,14 +1,16 @@
import ParentPage from '@/components/parentPage.vue';
// Settings
const Settings = () => import(/* webpackChunkName: "settings" */'@/components/settings/index');
const API = () => import(/* webpackChunkName: "settings" */'@/components/settings/api');
const DataExport = () => import(/* webpackChunkName: "settings" */'@/components/settings/dataExport');
const Notifications = () => import(/* webpackChunkName: "settings" */'@/components/settings/notifications');
const PromoCode = () => import(/* webpackChunkName: "settings" */'@/components/settings/promoCode');
const Site = () => import(/* webpackChunkName: "settings" */'@/components/settings/site');
const Settings = () => import(/* webpackChunkName: "settings" */'@/pages/settings-overview');
const GeneralSettings = () => import(/* webpackChunkName: "settings" */'@/pages/settings/generalSettings');
const Notifications = () => import(/* webpackChunkName: "settings" */'@/pages/settings/notificationSettings');
const Transactions = () => import(/* webpackChunkName: "settings" */'@/pages/settings/purchaseHistory.vue');
const SiteData = () => import(/* webpackChunkName: "settings" */'@/pages/settings/siteData.vue');
// not converted yet
const PromoCode = () => import(/* webpackChunkName: "settings" */'@/pages/settings/promoCode.vue');
const Subscription = () => import(/* webpackChunkName: "settings" */'@/components/settings/subscription');
const Transactions = () => import(/* webpackChunkName: "settings" */'@/components/settings/purchaseHistory');
export const USER_ROUTES = {
path: '/user',
@ -20,20 +22,16 @@ export const USER_ROUTES = {
component: Settings,
children: [
{
name: 'site',
path: 'site',
component: Site,
name: 'general',
path: 'general',
component: GeneralSettings,
},
{
name: 'api',
path: 'api',
component: API,
},
{
name: 'dataExport',
path: 'data-export',
component: DataExport,
name: 'siteData',
path: 'siteData',
component: SiteData,
},
{ path: 'api', redirect: { name: 'siteData' } },
{
name: 'promoCode',
path: 'promo-code',

View file

@ -65,7 +65,7 @@ export async function sleep (store) {
}
export async function addWebhook (store, payload) {
const response = await axios.post('/api/v4/user/webhook', payload.webhookInfo);
const response = await axios.post('/api/v4/user/webhook', payload.webhook);
return response.data.data;
}

View file

@ -2,6 +2,7 @@
const path = require('path');
const webpack = require('webpack');
const nconf = require('nconf');
const vueTemplateCompiler = require('vue-template-babel-compiler');
const { DuplicatesPlugin } = require('inspectpack/plugin');
const setupNconf = require('../server/libs/setupNconf');
const pkg = require('./package.json');
@ -126,6 +127,15 @@ module.exports = {
if (process.env.NODE_ENV === 'development') {
config.plugins.delete('preload');
}
// enable optional chaining in templates
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compiler = vueTemplateCompiler;
return options;
});
},
devServer: {

View file

@ -1,7 +1,5 @@
{
"frequentlyAskedQuestions": "Frequently Asked Questions",
"general": "General",
"faqQuestion0": "I'm confused. Where do I get an overview?",
"iosFaqAnswer0": "First, you'll set up tasks that you want to do in your everyday life. Then, as you complete the tasks in real life and check them off, you'll earn experience and gold. Gold is used to buy equipment and some items, as well as custom rewards. Experience causes your character to level up and unlock content such as Pets, Skills, and Quests! You can customize your character under Menu > Customize Avatar.\n\n Some basic ways to interact: click the (+) in the upper-right-hand corner to add a new task. Tap on an existing task to edit it, and swipe left on a task to delete it. You can sort tasks using Tags in the upper-left-hand corner, and expand and contract checklists by clicking on the checklist bubble.",
"androidFaqAnswer0": "First, you'll set up tasks that you want to do in your everyday life. Then, as you complete the tasks in real life and check them off, you'll earn experience and gold. Gold is used to buy equipment and some items, as well as custom rewards. Experience causes your character to level up and unlock content such as Pets, Skills, and Quests! You can customize your character under Menu > [Inventory >] Avatar.\n\n Some basic ways to interact: click the (+) in the lower-right-hand corner to add a new task. Tap on an existing task to edit it, and swipe left on a task to delete it. You can sort tasks using Tags in the upper-right-hand corner, and expand and contract checklists by clicking on the checklist count box.",

View file

@ -114,7 +114,7 @@
"missingPassword": "Missing password.",
"missingNewPassword": "Missing new password.",
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
"wrongPassword": "Wrong password.",
"wrongPassword": "Password is incorrect. If you forgot your password, click \"Forgot Password.\"",
"incorrectDeletePhrase": "Please type <%= magicWord %> in all capital letters to delete your account.",
"notAnEmail": "Invalid email address.",
"emailTaken": "Email address is already used in an account.",

View file

@ -24,6 +24,7 @@
"modalAchievement": "Achievement!",
"special": "Special",
"site": "Site",
"general": "General",
"help": "Help",
"user": "User",
"market": "Market",
@ -71,6 +72,7 @@
"error": "Error",
"menu": "Menu",
"notifications": "Notifications",
"allNotifications": "All Notifications",
"noNotifications": "You're all caught up!",
"noNotificationsText": "The notification fairies give you a raucous round of applause! Well done!",
"clear": "Clear",

View file

@ -1,18 +1,26 @@
{
"settings": "Settings",
"generalSettings": "General Settings",
"siteData": "Site Data",
"taskSettings": "Task Settings",
"confirmCancelChanges": "Are you sure? You will lose your unsaved changes.",
"account": "Account",
"loginMethods": "Login Methods",
"character": "Character",
"language": "Language",
"siteLanguage": "Site Language",
"americanEnglishGovern": "In the event of a discrepancy in the translations, the American English version governs.",
"helpWithTranslation": "Would you like to help with the translation of Habitica? Great! Then visit <a href=\"https://translate.habitica.com\">Habitica's Weblate site</a>!",
"helpWithTranslation": "Are you interested in helping with the translation of Habitica? Great! Then visit <a href=\"https://translate.habitica.com\">Habitica's Weblate site</a>!",
"stickyHeader": "Sticky header",
"newTaskEdit": "Open new tasks in edit mode",
"reverseChatOrder": "Show chat messages in reverse order",
"startAdvCollapsed": "Advanced Settings in tasks start collapsed",
"startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.",
"dontShowAgain": "Don't show this again",
"suppressLevelUpModal": "Don't show popup when gaining a level",
"suppressHatchPetModal": "Don't show popup when hatching a pet",
"suppressRaisePetModal": "Don't show popup when raising a pet into a mount",
"suppressStreakModal": "Don't show popup when attaining a Streak achievement",
"showLevelUpModal": "When Gaining a Level",
"showHatchPetModal": "When Hatching a Pet",
"showRaisePetModal": "When Raising a Pet into a Mount",
"showStreakModal": "When Attaining a Streak Achievement",
"baileyAnnouncement": "Latest Bailey Announcement",
"view": "View",
"showTour": "Show Tour",
"showBailey": "Show Bailey",
"showBaileyPop": "Bring Bailey the Town Crier out of hiding so you can review past news.",
@ -25,17 +33,28 @@
"resetAccPop": "Start over, removing all levels, gold, gear, history, and tasks.",
"deleteAccount": "Delete Account",
"deleteAccPop": "Cancel and remove your Habitica account.",
"feedback": "If you'd like to give us feedback, please enter it below - we'd love to know what you liked or didn't like about Habitica! Don't speak English well? No problem! Use the language you prefer.",
"feedback": "If you'd like to give us feedback, please enter it below - we'd love to hear your feedback! It will be anonymous unless you choose to enter your contact details. Don't speak English well? No problem! Use the language you prefer.",
"feedbackPlaceholder": "Add your feedback",
"dataExport": "Data Export",
"saveData": "Here are a few options for saving your data.",
"habitHistory": "Habit History",
"exportHistory": "Export History:",
"csv": "(CSV)",
"downloadCSV": "Download CSV",
"downloadAs": "Download as",
"userData": "User Data",
"yourUserData": "Your User Data",
"taskHistory": "Task History",
"yourUserDataDisclaimer": "Here you can download a copy of your task history or your full user data.",
"exportUserData": "Export User Data:",
"useridCopied": "User ID copied to clipboard.",
"userIdTooltip": "The User ID is a unique number that Habitica automatically generates when a player joins, similar to a Username. However, unlike the Username, a User ID can not be changed.",
"developerMode": "Developer Mode",
"developerModeTooltip": "Habitica provides a developer mode to enable additional features that interact with Habitica's API.",
"export": "Export",
"xml": "(XML)",
"json": "(JSON)",
"xml": "XML",
"json": "JSON",
"api": "API",
"customDayStart": "Custom Day Start",
"adjustment": "Adjustment",
"dayStartAdjustment": "Day Start Adjustment",
@ -45,6 +64,7 @@
"customDayStartInfo1": "Habitica checks and resets your Dailies at midnight in your own time zone each day. You can adjust when that happens past the default time here.",
"misc": "Misc",
"showHeader": "Show Header",
"currentPass": "Current Password",
"changePass": "Change Password",
"changeUsername": "Change Username",
"changeEmail": "Change Email Address",
@ -54,11 +74,18 @@
"confirmPass": "Confirm New Password",
"newUsername": "New Username",
"dangerZone": "Danger Zone",
"resetText1": "WARNING! This resets many parts of your account. This is highly discouraged, but some people find it useful in the beginning after playing with the site for a short time.",
"resetText2": "You will lose all your levels, Gold, and Experience points. All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data. You will lose all your equipment except Subscriber Mystery Items and free commemorative items. You will be able to buy the deleted items back, including all limited edition equipment (you will need to be in the correct class to re-buy class-specific gear). You will keep your current class, achievements and your pets and mounts. You might prefer to use an Orb of Rebirth instead, which is a much safer option and which will preserve your tasks and equipment.",
"deleteLocalAccountText": "Are you sure? This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type your password into the text box below.",
"deleteSocialAccountText": "Are you sure? This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type \"<%= magicWord %>\" into the text box below.",
"resetText1": "<b>Be careful!</b> This resets many parts of your account. This is highly discouraged, but some people find it useful in the beginning after playing with the site for a short time.",
"resetDetail1": "You will lose all your levels, Gold, and Experience points.",
"resetDetail2": "You will keep your current class, achievements and your pets and mounts.",
"resetDetail3": "All your tasks (except those from challenges) will be deleted permanently and you will lose all of their historical data.",
"resetDetail4": "You will lose all your equipment except Subscriber Mystery Items and free commemorative items. You will be able to buy the deleted items back, including all limited edition equipment (you will need to be in the correct class to re-buy class-specific gear).",
"resetText2": "Another option is using an <b>Orb of Rebirth</b>, which will reset everything else while preserving your Tasks and Equipment.",
"deleteLocalAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type your password into the text box below.",
"deleteSocialAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type <b>\"<%= magicWord %>\"</b> into the text box below.",
"API": "API",
"APICopied": "API token copied to clipboard.",
"APITokenTitle": "API Token",
"APITokenDisclaimer": "<b>Your API Token is like a password; Do not share it publicly.</b> You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.<br><br><b>Note:</b> If you need a new API Token (e.g., if you accidentally shared it), email <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> with your User ID and current Token. Once it is reset you will need to re-authorize everything by logging out of the website and mobile app and by providing the new Token to any other Habitica tools that you use.",
"APIv3": "API v3",
"APIText": "Copy these for use in third party applications. However, think of your API Token like a password, and do not share it publicly. You may occasionally be asked for your User ID, but never post your API Token where others can see it, including on Github.",
"APIToken": "API Token (this is a password - see warning above!)",
@ -70,13 +97,14 @@
"resetDo": "Do it, reset my account!",
"resetComplete": "Reset complete!",
"fixValues": "Fix Values",
"fixValuesText1": "If you've encountered a bug or made a mistake that unfairly changed your character (damage you shouldn't have taken, Gold you didn't really earn, etc.), you can manually correct your numbers here. Yes, this makes it possible to cheat: use this feature wisely, or you'll sabotage your own habit-building!",
"fixValuesText2": "Note that you cannot restore Streaks on individual tasks here. To do that, edit the Daily and go to Advanced Settings, where you will find a Restore Streak field.",
"fixValuesText1": "If you&apos;ve encountered an issue that unfairly changed your character (damage you shouldn&apos;t have taken, Gold you didn&apos;t really earn, etc.), you can manually correct those values here. Yes, this makes it possible to cheat: use this feature wisely, or you&apos;ll sabotage your own habit-building!",
"fixValuesText2": "<b>Note</b>: To restore Streaks on individual Tasks, edit the Task and use the Restore Streak field.",
"fix21Streaks": "21-Day Streaks",
"discardChanges": "Discard Changes",
"deleteDo": "Do it, delete my account!",
"invalidPasswordResetCode": "The supplied password reset code is invalid or has expired.",
"passwordChangeSuccess": "Your password was successfully changed to the one you just chose. You can now use it to access your account.",
"userNameSuccess": "Username successfully changed",
"displayNameSuccess": "Display name successfully changed",
"emailSuccess": "Email successfully changed",
"passwordSuccess": "Password successfully changed",
@ -98,8 +126,8 @@
"giftedSubscriptionInfo": "<%= name %> gifted you a <%= months %> month subscription",
"giftedSubscriptionFull": "Hello <%= username %>, <%= sender %> has sent you <%= monthCount %> months of subscription!",
"giftedSubscriptionWinterPromo": "Hello <%= username %>, you received <%= monthCount %> months of subscription as part of our holiday gift-giving promotion!",
"invitedParty": "You were invited to a Party",
"invitedGuild": "You were invited to a Guild",
"invitedParty": "Invited to a Party",
"invitedGuild": "Invited to a Guild",
"importantAnnouncements": "Reminders to check in to complete tasks and receive prizes",
"weeklyRecaps": "Summaries of your account activity in the past week (Note: this is currently disabled due to performance issues, but we hope to have this back up and sending e-mails again soon!)",
"onboarding": "Guidance with setting up your Habitica account",
@ -107,25 +135,24 @@
"subscriptionReminders": "Subscriptions Reminders",
"questStarted": "Your Quest has Begun",
"invitedQuest": "Invited to Quest",
"kickedGroup": "Kicked from group",
"kickedGroup": "Removed from group",
"remindersToLogin": "Reminders to check in to Habitica",
"unsubscribedSuccessfully": "Unsubscribed successfully!",
"unsubscribedTextUsers": "You have successfully unsubscribed from all Habitica emails. You can enable only the emails you want to receive from <a href=\"/user/settings/notifications\">Settings > &gt; Notifications</a> (requires login).",
"unsubscribedTextOthers": "You won't receive any other email from Habitica.",
"unsubscribeAllEmails": "Check to Unsubscribe from Emails",
"unsubscribeAllEmailsText": "By checking this box, I certify that I understand that by unsubscribing from all emails, Habitica will never be able to notify me via email about important changes to the site or my account.",
"unsubscribeAllPush": "Check to Unsubscribe from all Push Notifications",
"unsubscribeAllEmails": "Unsubscribe from Emails",
"unsubscribeAllEmailsText": "Habitica will be unable to notify you via email about important changes to the site or your account.",
"unsubscribeAllPush": "Unsubscribe from all Push Notifications",
"correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.",
"subscriptionRateText": "Recurring <strong>$<%= price %> USD</strong> every <strong><%= months %> months</strong>",
"giftSubscriptionRateText": "<strong>$<%= price %> USD</strong> for <strong><%= months %> months</strong>",
"benefits": "Benefits",
"coupon": "Coupon",
"couponText": "We sometimes have events and give out promo codes for special gear. (eg, those who stop by our Wondercon booth)",
"couponText": "We sometimes have events and give out promo codes for special gear.",
"apply": "Apply",
"promoCode": "Promo Code",
"promoCodeApplied": "Promo Code Applied! Check your inventory",
"promoPlaceholder": "Enter Promotion Code",
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
"saveCustomDayStart": "Save Custom Day Start",
"registration": "Registration",
"addLocalAuth": "Add Email and Password Login",
@ -134,9 +161,10 @@
"generate": "Generate",
"getCodes": "Get Codes",
"webhooks": "Webhooks",
"webhooksInfo": "Habitica provides webhooks so that when certain actions occur in your account, information can be sent to a script on another website. You can specify those scripts here. Be careful with this feature because specifying an incorrect URL can cause errors or slowness in Habitica. For more information, see the wiki's <a target=\"_blank\" href=\"https://habitica.fandom.com/wiki/Webhooks\">Webhooks</a> page.",
"webhooksInfo": "Webhooks provide a way for developers to receive notifications when a particular action is performed, such as scoring or updating a Task, or sending a message in a Group. By creating a webhook, you will be able to listen to changes in Habitica and build apps that respond to these changes.<br><br>For additional information and examples on webhooks, please visit our <a target=\"_blank\" href=\"https://habitica.fandom.com/wiki/Webhooks\">API Docs</a>",
"enabled": "Enabled",
"webhookURL": "Webhook URL",
"addWebhook": "Add Webhook",
"invalidUrl": "invalid url",
"invalidWebhookId": "the \"id\" parameter should be a valid UUID.",
"webhookBooleanOption": "\"<%= option %>\" must be a Boolean value.",
@ -179,7 +207,14 @@
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
"usernameNotVerified": "Please confirm your username.",
"changeUsernameDisclaimer": "Your username is used for invitations, @mentions in chat, and messaging. It must be 1 to 20 characters, containing only letters a to z, numbers 0 to 9, hyphens, or underscores, and cannot include any inappropriate terms.",
"changeEmailDisclaimer": "This is the email address that you use to log in to Habitica, as well as receive notifications.",
"changeDisplayNameDisclaimer": "This is the name that will be displayed for your Avatar in Habitica.",
"changePasswordDisclaimer": "Password must be 8 characters or more. We recommend a strong password that you're not using elsewhere.",
"dateFormatDisclaimer": "Adjust the date formatting across Habitica.",
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!",
"enableAudio": "Enable Audio",
"playDemoAudio": "Play Demo",
"audioThemeDisclaimer": "Audio themes add optional sound effects to the Habitica website. Volume levels are controlled using your computer's volume settings.",
"mentioning": "Mentioning",
"suggestMyUsername": "Suggest my username",
"everywhere": "Everywhere",
@ -189,6 +224,11 @@
"amount": "Amount",
"action": "Action",
"note": "Note",
"noClassSelected": "No Class Selected",
"currentClass": "Current Class",
"changeClassSetting": "Change Class",
"chooseClassSetting": "Choose Class",
"changeClassDisclaimer": "Changing your class will refund all of your existing Stat Points. Once you have selected your new class, adjust your Stat Points from the Stats section of your profile.",
"remainingBalance": "Remaining Balance",
"transactions": "Transactions",
"hourglassTransactions": "Hourglass Transactions",
@ -203,7 +243,6 @@
"transaction_gift_receive": "<b>Received</b> from",
"transaction_create_challenge": "<b>Created</b> challenge",
"transaction_create_bank_challenge": "<b>Created</b> bank challenge",
"transaction_create_bank_challenge": "Created bank challenge",
"transaction_create_guild": "<b>Created</b> guild",
"transaction_change_class": "<b>Class</b> change",
"transaction_rebirth": "Used Orb of Rebirth",
@ -212,5 +251,8 @@
"transaction_reroll": "Used Fortify Potion",
"transaction_subscription_perks": "<b>Subscription</b> perk",
"transaction_admin_update_balance": "<b>Admin</b> given",
"transaction_admin_update_hourglasses": "<b>Admin</b> updated"
"transaction_admin_update_hourglasses": "<b>Admin</b> updated",
"connected": "Connected",
"connect": "Connect",
"remove": "Remove"
}

View file

@ -93,6 +93,9 @@
"streakCoins": "Streak Bonus!",
"taskToTop": "To top",
"taskToBottom": "To bottom",
"taskAlias": "Task Alias",
"taskAliasPopover": "This task alias can be used when integrating with 3rd party integrations. Only dashes, underscores, and alphanumeric characters are supported. The task alias must be unique among all your tasks.",
"taskAliasPlaceholder": "your-task-alias-here",
"taskAliasAlreadyUsed": "Task alias already used on another task.",
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".",

View file

@ -21,8 +21,8 @@ export const CHAT_FLAG_FROM_SHADOW_MUTE = 10;
// @TODO use those constants to replace hard-coded numbers
export const SUPPORTED_SOCIAL_NETWORKS = [
{ key: 'google', name: 'Google' },
{ key: 'apple', name: 'Apple' },
{ key: 'google', name: 'Google' },
];
export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination

View file

@ -1,12 +1,9 @@
import moment from 'moment';
export const CURRENT_SEASON = moment().isBefore('2020-08-02') ? 'summer' : '_NONE_';
// sorting this also changes the class selection
export const CLASSES = [
'warrior',
'rogue',
'healer',
'wizard',
'rogue',
'warrior',
];
export const GEAR_TYPES = [

View file

@ -12,7 +12,6 @@ export default function addTask (user, req = { body: {} }) {
if (task._editing) {
task._edit = clone(task);
}
task._advanced = !user.preferences.advancedCollapsed;
return task;
}

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