Squashed commit of the following:

commit 16d8b87e90
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 07387faf48
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 ea8563cd17
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 3e16584dcf
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 21:22:06 2023 +0200

    fix PR comments

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

    fix PR comments

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

    update form.scss

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

    remove ending

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

    fix spacing

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

    clean up debug row for login methods

commit 41cde37675
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 8c568060f9
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 21:49:31 2023 +0200

    fix PR comments

commit 36f7a4711d
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 d279af7897
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 b20ea44d49
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Aug 9 21:04:12 2023 +0200

    Split Vue.Router routes

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

    remove console

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

    update Bailey Notification Text + fix popover

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

    fix change password setting

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

    fix reset account texts

commit ccfdd9bb9c
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 35c75304f1
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 2 20:16:06 2023 +0200

    more fixes

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

    fix notification settings

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

    applied same styling to promoCode.vue

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

    move promoCode.vue to pages/settings

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

    saveCancelButtons.vue allow to hide the cancel part

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

    Hide Transactions Page again

commit 8b5ae17f02
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 aa97ed5299
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:25:53 2023 +0200

    fix localhost externalLinks check

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

    show notification on username change + fix userEmail checks

commit 6a6f55f6fc
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 f9ff5e5c55
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 4497514eeb
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:59:21 2023 +0200

    show notification when chaning display name

commit 3232f12f0d
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 582a2f1304
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 8e3b8a962a
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:24:46 2023 +0200

    refactor currentPasswordInput.vue to use validatedTextInput.vue

commit 61521507a4
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 f74c29a065
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 c4b6f0c39c
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 37eee140ad
Author: negue <eugen.bolz@gmail.com>
Date:   Fri May 12 21:57:27 2023 +0200

    delete account without password

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

    fix duplicate json entry

commit 47a2189f49
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 a56b4a4457
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:37:31 2023 +0200

    show current class on setting panel

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

    fix selectDifficulty.vue - refactor selectList.vue

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

    migrate restoreValues fix to new setting component

commit 7947b1c67d
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 ad3e4d604a
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Apr 29 01:18:25 2023 +0200

    style fixes

commit cea13d5bc3
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 73a5e5fcab
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 25 20:51:14 2023 +0200

    style / padding issues

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

    fix "setting new password" invalid check

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

    add password for other logins

commit 9ff17fd6dd
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 1f470942a9
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:19:18 2023 +0200

    delete old api.vue

commit b4904a8b84
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 b5da7ccc70
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:11:36 2023 +0200

    refactor webhook ui to use save/cancel buttons

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

    remove unused settings

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

    remove advancedCollapsed settings to start it opened

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

    remove displayInviteToPartyWhenPartyIs1 setting

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

    webhook margins

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

    userid tooltip

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

    remove balance for choosing class

commit 160848473d
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 f74ba9738d
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:10:53 2023 +0100

    update apple icon and size

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

    Copied API Token Notification

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

    remove blue color of setting links

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

    fix username/email setting input width

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

    developer mode

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

    Add new Pause Dailies Setting

commit 576c80af7e
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 dec1a1159d
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:00:52 2023 +0100

    developer mode dummy row

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

    WIP webhook row

commit cc4bedbe2d
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 f9833aa78a
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 9 02:23:39 2023 +0100

    API Token Row

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

    "Your User Data" Row instead of Page

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

    userid row

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

    rename folder of setting rows

commit 3b050861c4
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 b09298fb01
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 5ed25066ec
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 20:06:13 2023 +0100

    size/margin for transactions

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

    move purchaseHistory.vue

commit 8e4e1bcb0f
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 85c50d50e9
Author: SabreCat <sabe@habitica.com>
Date:   Thu Feb 16 14:23:27 2023 -0600

    fix(css): remove redundant formatting for a elements

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

    remove console

commit 8c5e722c72
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 9c8770051d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 19:13:16 2023 +0100

    fix dayStartAdjustmentSetting.vue for 0 value

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

    fix color after refactor

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

    mark audioThemeSetting as changed

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

    fix ul/li style in resetAccount.vue

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

    fix color of gem price

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

    fix "fixValuesSetting.vue"

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

    open forgot password in new tab

commit 31edec9ec5
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 2adfd8c259
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 20:19:30 2023 +0100

    hide class setting until level 10

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

    delete old modals (refactored into new settings ui)

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

    enable forgot password link in settings

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

    reset account + password required in api

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

    fix compile

commit 7ffb5101be
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 2bfb130b92
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 89530a133c
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 18 19:22:36 2023 +0100

    wip fix values

commit 428647fc71
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 1f16819bc1
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 11 22:41:05 2023 +0100

    WIP fix values

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

    check for unsaved changes when pressing cancel

commit bef8a4cdfc
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 494f32c3e3
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 21 00:55:31 2022 +0100

    Class Setting

commit bda210cfbb
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 38198d7df6
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 22:36:27 2022 +0100

    WIP class setting

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

    fix styles

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

    Scroll into opened Setting

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

    Audio Theme Setting

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

    prepare header settings but still hidden

commit 30d8b27534
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 a1d1a788b2
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 23:34:33 2022 +0100

    DayStartAdjustmentSetting

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

    disable reset account button when password empty

commit 30a6db4c2d
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 78093848d7
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 7 22:19:15 2022 +0100

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

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

    re-enable box-shadow on hover

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

    remove console log

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

    dateFormatSetting

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

    move shared components / mixins

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

    language Setting + imports cleanup

commit 29ebd89030
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 5c7747517b
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 fd5cbc3026
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:14:21 2022 +0100

    fix conflicts

commit 49361217b0
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 edb427158f
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:03:19 2022 +0100

    disable save button if nothing was changed

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

    delete account row

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

    Shared Modal Visible State

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

    resetAccount

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

    rename functional components for compiler

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

    password setting row

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

    update package-lock.json again

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

    update package-lock.json

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

    displayNameSetting.vue

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

    fix lint

commit a2b510caca
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 0bae5fbe02
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 22:00:34 2022 +0100

    userEmailSetting

commit 23da70fa2e
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 82047380f3
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 20:18:21 2022 +0100

    first setting (username) in the new layout

commit 39150349c7
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 f7787b318c
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 4c0ecc9938
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 2f53613a45
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Oct 10 22:54:41 2022 +0200

    split routes for ease of dev

commit 390f0fc69d
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 cf222ee63a
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 23:15:35 2022 +0200

    Update remaining Notification labels

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

    move site popup settings to notifications

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

    fix styling in notification settings

commit 7b5568ed23
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-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0", "gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0", "gulp-nodemon": "^2.5.0",
"nodemon": "^2.0.20",
"gulp.spritesmith": "^6.13.0", "gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0", "habitica-markdown": "^3.0.0",
"helmet": "^4.6.0", "helmet": "^4.6.0",

View File

@@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
type: 'habit', type: 'habit',
}); });
await user.post('/user/reset'); await user.post('/user/reset', {
password: 'password',
});
await user.sync(); await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
type: 'daily', type: 'daily',
}); });
await user.post('/user/reset'); await user.post('/user/reset', {
password: 'password',
});
await user.sync(); await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
type: 'todo', type: 'todo',
}); });
await user.post('/user/reset'); await user.post('/user/reset', {
password: 'password',
});
await user.sync(); await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
type: 'reward', type: 'reward',
}); });
await user.post('/user/reset'); await user.post('/user/reset', {
password: 'password',
});
await user.sync(); await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ 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; 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 () => { it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' }); const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const challenge = await generateChallenge(user, guild); 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(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset'); await user.post('/user/reset', {
password: 'password',
});
await user.sync(); await user.sync();
await user.put('/user', { 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}`); 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)._editing).not.be.ok;
expect(addTask(user)._edit).to.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/no-unresolved': 'off',
'import/extensions': 'off', 'import/extensions': 'off',
'vue/no-v-html': 'off', 'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
'vue/html-self-closing': ['error', { 'vue/html-self-closing': ['error', {
html: { html: {
void: 'never', void: 'never',

View File

@@ -55,3 +55,13 @@ in a separate `.add('function of component', ...`
### Storybook Build ### Storybook Build
After each client build, storybook build is also triggered and will be available in `dist/storybook` 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": { "dependencies": {
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
} }
} }
}, },
"assert": { "assert": {
"version": "1.5.0", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
"integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==",
"requires": { "requires": {
"object-assign": "^4.1.1", "object.assign": "^4.1.4",
"util": "0.10.3" "util": "^0.10.4"
}, },
"dependencies": { "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": { "inherits": {
"version": "2.0.1", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" "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": { "util": {
"version": "0.10.3", "version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"requires": { "requires": {
"inherits": "2.0.1" "inherits": "2.0.3"
} }
} }
} }
@@ -15543,9 +15568,9 @@
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
}, },
"bn.js": { "bn.js": {
"version": "5.1.3", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
"integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==" "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
}, },
"bonjour": { "bonjour": {
"version": "3.5.0", "version": "3.5.0",
@@ -15762,7 +15787,7 @@
"brorand": { "brorand": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
}, },
"browser-process-hrtime": { "browser-process-hrtime": {
"version": "1.0.0", "version": "1.0.0",
@@ -15834,9 +15859,9 @@
}, },
"dependencies": { "dependencies": {
"readable-stream": { "readable-stream": {
"version": "3.6.0", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@@ -15915,12 +15940,12 @@
"buffer-xor": { "buffer-xor": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="
}, },
"builtin-status-codes": { "builtin-status-codes": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", "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": { "cacache": {
"version": "12.0.3", "version": "12.0.3",
@@ -16793,7 +16818,7 @@
"constants-browserify": { "constants-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", "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": { "contains-path": {
"version": "0.1.0", "version": "0.1.0",
@@ -16907,9 +16932,9 @@
} }
}, },
"core-js": { "core-js": {
"version": "3.32.1", "version": "3.32.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.32.2.tgz",
"integrity": "sha512-lqufgNn9NLnESg5mQeYsxQP5w7wrViSj0jr/kv6ECQiByzQkrn1MKvV0L3acttpDqfQrHLwr2KCMgX5b8X+lyQ==" "integrity": "sha512-pxXSw1mYZPDGvTQqEc5vgIb83jGQKFGYWY76z4a7weZXUolw3G+OvpZqSRcfYOoOVUQJYEPsWeQK8pKEnUtWxQ=="
}, },
"core-js-compat": { "core-js-compat": {
"version": "3.11.0", "version": "3.11.0",
@@ -17037,9 +17062,9 @@
}, },
"dependencies": { "dependencies": {
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
} }
} }
}, },
@@ -17376,7 +17401,7 @@
"de-indent": { "de-indent": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=" "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="
}, },
"debug": { "debug": {
"version": "4.1.1", "version": "4.1.1",
@@ -17670,9 +17695,9 @@
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
}, },
"des.js": { "des.js": {
"version": "1.0.1", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
"integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"requires": { "requires": {
"inherits": "^2.0.1", "inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0" "minimalistic-assert": "^1.0.0"
@@ -17819,9 +17844,9 @@
}, },
"dependencies": { "dependencies": {
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
} }
} }
}, },
@@ -18598,13 +18623,29 @@
} }
}, },
"eslint-plugin-vue": { "eslint-plugin-vue": {
"version": "6.2.2", "version": "7.20.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-6.2.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.20.0.tgz",
"integrity": "sha512-Nhc+oVAHm0uz/PkJAWscwIT4ijTrK5fqNqz9QB1D35SbbuMG1uB6Yr5AJpvPSWg+WOw7nYNswerYh0kOk64gqQ==", "integrity": "sha512-oVNDqzBC9h3GO+NTgWeLMhhGigy6/bQaQbHS+0z7C4YEu/qK/yxHvca/2PTZtGNPsCrHwOTgKMrwu02A9iPBmw==",
"requires": { "requires": {
"eslint-utils": "^2.1.0",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"semver": "^5.6.0", "semver": "^6.3.0",
"vue-eslint-parser": "^7.0.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": { "eslint-scope": {
@@ -20578,9 +20619,9 @@
}, },
"dependencies": { "dependencies": {
"readable-stream": { "readable-stream": {
"version": "3.6.0", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"requires": { "requires": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@@ -20724,7 +20765,7 @@
"hmac-drbg": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"requires": { "requires": {
"hash.js": "^1.0.3", "hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0", "minimalistic-assert": "^1.0.0",
@@ -21030,7 +21071,7 @@
"https-browserify": { "https-browserify": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "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": { "https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
@@ -21886,7 +21927,7 @@
"is-window": { "is-window": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-window/-/is-window-1.0.2.tgz",
"integrity": "sha1-LIlspT25feRdPDMTOmXYyfVjSA0=" "integrity": "sha512-uj00kdXyZb9t9RcAUAwMZAnkBUwdYGhYlt7djMXhfyhUCzwNba50tIiBKR7q0l7tdoBtFVw/3JmLY6fI3rmZmg=="
}, },
"is-windows": { "is-windows": {
"version": "1.0.2", "version": "1.0.2",
@@ -23254,9 +23295,9 @@
}, },
"dependencies": { "dependencies": {
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
} }
} }
}, },
@@ -23323,7 +23364,7 @@
"minimalistic-crypto-utils": { "minimalistic-crypto-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
@@ -24388,7 +24429,7 @@
"punycode": { "punycode": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "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": { "os-browserify": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "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": { "os-homedir": {
"version": "1.0.2", "version": "1.0.2",
@@ -25089,9 +25130,9 @@
"integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
}, },
"pbkdf2": { "pbkdf2": {
"version": "3.1.1", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
"integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
"requires": { "requires": {
"create-hash": "^1.1.2", "create-hash": "^1.1.2",
"create-hmac": "^1.1.4", "create-hmac": "^1.1.4",
@@ -26147,9 +26188,9 @@
}, },
"dependencies": { "dependencies": {
"bn.js": { "bn.js": {
"version": "4.11.9", "version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
} }
} }
}, },
@@ -26350,7 +26391,7 @@
"querystring-es3": { "querystring-es3": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
"integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA=="
}, },
"querystringify": { "querystringify": {
"version": "2.2.0", "version": "2.2.0",
@@ -29503,7 +29544,7 @@
"to-arraybuffer": { "to-arraybuffer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "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": { "to-fast-properties": {
"version": "2.0.0", "version": "2.0.0",
@@ -29779,7 +29820,7 @@
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
"integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=" "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw=="
}, },
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
@@ -30249,7 +30290,7 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "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": { "uuid-browser": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid-browser/-/uuid-browser-3.1.0.tgz", "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": { "v8-compile-cache": {
"version": "2.1.0", "version": "2.1.0",
@@ -30579,29 +30620,90 @@
} }
}, },
"vue-eslint-parser": { "vue-eslint-parser": {
"version": "7.0.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.0.0.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz",
"integrity": "sha512-yR0dLxsTT7JfD2YQo9BhnQ6bUTLsZouuzt9SKRP7XNaZJV459gvlsJo4vT2nhZ/2dH9j3c53bIx9dnqU2prM9g==", "integrity": "sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg==",
"requires": { "requires": {
"debug": "^4.1.1", "debug": "^4.1.1",
"eslint-scope": "^5.0.0", "eslint-scope": "^5.1.1",
"eslint-visitor-keys": "^1.1.0", "eslint-visitor-keys": "^1.1.0",
"espree": "^6.1.2", "espree": "^6.2.1",
"esquery": "^1.0.1", "esquery": "^1.4.0",
"lodash": "^4.17.15" "lodash": "^4.17.21",
"semver": "^6.3.0"
}, },
"dependencies": { "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": { "eslint-scope": {
"version": "5.0.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"requires": { "requires": {
"esrecurse": "^4.1.0", "esrecurse": "^4.3.0",
"estraverse": "^4.1.1" "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": { "vue-functional-data-merge": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz", "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": { "vue-template-compiler": {
"version": "2.7.10", "version": "2.7.10",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.10.tgz",
@@ -30728,9 +31164,9 @@
}, },
"dependencies": { "dependencies": {
"anymatch": { "anymatch": {
"version": "3.1.1", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"optional": true, "optional": true,
"requires": { "requires": {
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -30753,19 +31189,19 @@
} }
}, },
"chokidar": { "chokidar": {
"version": "3.5.1", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==", "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"optional": true, "optional": true,
"requires": { "requires": {
"anymatch": "~3.1.1", "anymatch": "~3.1.2",
"braces": "~3.0.2", "braces": "~3.0.2",
"fsevents": "~2.3.1", "fsevents": "~2.3.2",
"glob-parent": "~5.1.0", "glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0", "is-binary-path": "~2.1.0",
"is-glob": "~4.0.1", "is-glob": "~4.0.1",
"normalize-path": "~3.0.0", "normalize-path": "~3.0.0",
"readdirp": "~3.5.0" "readdirp": "~3.6.0"
} }
}, },
"fill-range": { "fill-range": {
@@ -30778,15 +31214,15 @@
} }
}, },
"fsevents": { "fsevents": {
"version": "2.3.1", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"optional": true "optional": true
}, },
"glob-parent": { "glob-parent": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"optional": true, "optional": true,
"requires": { "requires": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
@@ -30808,9 +31244,9 @@
"optional": true "optional": true
}, },
"readdirp": { "readdirp": {
"version": "3.5.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"optional": true, "optional": true,
"requires": { "requires": {
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
@@ -30863,9 +31299,9 @@
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
}, },
"webpack": { "webpack": {
"version": "4.46.0", "version": "4.47.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.47.0.tgz",
"integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", "integrity": "sha512-td7fYwgLSrky3fI1EuU5cneU4+pbH6GgOfuKNS1tNPcfdGinGELAqsb/BP4nnvZyKSG2i/xFGU7+n2PvZA8HJQ==",
"requires": { "requires": {
"@webassemblyjs/ast": "1.9.0", "@webassemblyjs/ast": "1.9.0",
"@webassemblyjs/helper-module-context": "1.9.0", "@webassemblyjs/helper-module-context": "1.9.0",
@@ -30890,37 +31326,6 @@
"terser-webpack-plugin": "^1.4.3", "terser-webpack-plugin": "^1.4.3",
"watchpack": "^1.7.4", "watchpack": "^1.7.4",
"webpack-sources": "^1.4.1" "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": { "webpack-bundle-analyzer": {

View File

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

View File

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

View File

@@ -7,6 +7,10 @@
.dropdown-toggle:hover { .dropdown-toggle:hover {
--caret-color: #{$purple-300}; --caret-color: #{$purple-300};
&.disabled {
pointer-events: none;
}
} }
.dropdown.show > .dropdown-toggle:not(.btn-success) { .dropdown.show > .dropdown-toggle:not(.btn-success) {
@@ -136,6 +140,8 @@
.dropdown-menu.show { .dropdown-menu.show {
min-width: 100% !important; 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; color: $gray-50;
border: 1px solid $gray-400; border: 1px solid $gray-400;
&:hover:not(:disabled) { &:hover:not(:disabled):not(:read-only) {
border-color: $gray-300; 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; border-color: $purple-400;
outline: 0; outline: 0;
box-shadow: none; box-shadow: none;
@@ -56,13 +56,13 @@ input, textarea, input.form-control, textarea.form-control {
&.input-valid, &.input-invalid { &.input-valid, &.input-invalid {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center right 16px; background-position: center right 0.5rem;
} }
&.input-valid { &.input-valid {
padding-right: 37px; padding-right: 27px;
background-image: url(~@/assets/svg/for-css/check.svg); background-image: url(~@/assets/svg/for-css/check.svg);
background-size: 13px 10px; background-size: 1rem;
} }
&.input-invalid { &.input-invalid {
@@ -91,8 +91,10 @@ input, textarea, input.form-control, textarea.form-control {
border-color: $gray-300; border-color: $gray-300;
} }
&:focus, &:active, &:focus-within { &:not(:read-only) {
border: solid 1px $purple-400; &:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
}
} }
.input-group-prepend , .input-group-append { .input-group-prepend , .input-group-append {
@@ -163,8 +165,22 @@ input, textarea, input.form-control, textarea.form-control {
input { input {
height: 30px; height: 30px;
border: 0; 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 { .input-group-spaced {
@@ -231,20 +247,20 @@ $bg-disabled-control: $gray-10;
background-color: inherit; 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 { &:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300; border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5); 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 { &:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5); box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
border-color: 2 px solid $purple-400; border-color: 2 px solid $purple-400;
background-color: $purple-400; background-color: $purple-400;
} }
&:focus:disabled~.custom-control-label::before, &:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before { &:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1); box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
} }
@@ -398,8 +414,6 @@ $bg-color: $purple-400;
margin-top: 0 !important; margin-top: 0 !important;
} }
// Disable default style Firefox for invalid elements. // Disable default style Firefox for invalid elements.
// Selectors taken from view-source:resource://gre-resources/forms.css on Firefox // Selectors taken from view-source:resource://gre-resources/forms.css on Firefox
:not(output):-moz-ui-invalid { :not(output):-moz-ui-invalid {

View File

@@ -1,55 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
<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"/>
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> </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"> <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">
<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"/> <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> </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 () { mounted () {
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
hello.init({ hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line 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 closeX from '../ui/closeX';
import copyIcon from '@/assets/svg/copy.svg'; import copyIcon from '@/assets/svg/copy.svg';
import copyToClipboard from '@/mixins/copyToClipboard';
export default { export default {
components: { components: {
closeX, closeX,
}, },
mixins: [notifications], mixins: [notifications, copyToClipboard],
data () { data () {
return { return {
icons: Object.freeze({ icons: Object.freeze({
@@ -287,17 +288,10 @@ export default {
this.$root.$emit('bv::hide::modal', 'create-party-modal'); this.$root.$emit('bv::hide::modal', 'create-party-modal');
}, },
copyUsername () { copyUsername () {
if (navigator.clipboard) { this.mixinCopyToClipboard(
navigator.clipboard.writeText(this.user.auth.local.username); this.user.auth.local.username,
} else { this.$t('usernameCopied'),
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'));
}, },
seekParty () { seekParty () {
this.$store.dispatch('user:set', { this.$store.dispatch('user:set', {

View File

@@ -86,11 +86,6 @@
color: $gray-50; color: $gray-50;
} }
.input-error {
color: $red-50;
font-size: 90%;
}
.input-group { .input-group {
border-radius: 2px; border-radius: 2px;
border: solid 1px $gray-400; 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 { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
import notificationsIcon from '@/assets/svg/notifications.svg'; import notificationsIcon from '@/assets/svg/notifications.svg';
import MenuDropdown from '../ui/customMenuDropdown'; import MenuDropdown from '../ui/customMenuDropdown';
import MessageCount from './messageCount'; import MessageCount from './messageCount.functional.vue';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager'; import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
import successImage from '@/assets/svg/success.svg'; import successImage from '@/assets/svg/success.svg';
import starBadge from '@/assets/svg/star-badge.svg'; import starBadge from '@/assets/svg/star-badge.svg';
// Notifications // Notifications
import CARD_RECEIVED from './notifications/cardReceived'; 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 GIFT_ONE_GET_ONE from './notifications/g1g1';
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned'; import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed'; import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@
:key="item.key" :key="item.key"
:item="item" :item="item"
:price="item.value" :price="item.value"
:item-content-class="'shop_'+item.key" :item-content-class="`shop_${item.key}`"
:empty-item="false" :empty-item="false"
:popover-position="'top'" :popover-position="'top'"
@click="featuredItemSelected(item)" @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" :item="item"
:price="item.goldValue ? item.goldValue : item.value" :price="item.goldValue ? item.goldValue : item.value"
:price-type="item.goldValue ? 'gold' : 'gem'" :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" :empty-item="false"
:popover-position="'top'" :popover-position="'top'"
@click="selectItem(item)" @click="selectItem(item)"

View File

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

View File

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

View File

@@ -682,7 +682,9 @@ export default {
// as default filter for daily // as default filter for daily
// and set the filter as 'due' only when the component first // and set the filter as 'due' only when the component first
// loads and not on subsequent reloads. // 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 filter = 'due'; // eslint-disable-line no-param-reassign
} }

View File

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

View File

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

View File

@@ -203,16 +203,15 @@
<template <template
v-if="task.type !== 'reward'" v-if="task.type !== 'reward'"
> >
<div class="d-flex mt-3"> <div class="d-flex mt-3 align-items-center">
<lockable-label <lockable-label
:locked="challengeAccessRequired" :locked="challengeAccessRequired"
:text="$t('difficulty')" :text="$t('difficulty')"
/> />
<div <information-icon
v-b-tooltip.hover.righttop="$t('difficultyHelp')" tooltip-id="difficultyHelp"
class="svg-icon info-icon mb-auto ml-1" :tooltip="$t('difficultyHelp')"
v-html="icons.information" />
></div>
</div> </div>
<select-difficulty <select-difficulty
:value="task.priority" :value="task.priority"
@@ -452,7 +451,7 @@
> >
<div> <div>
<div <div
v-if="task.type === 'daily' && isUserTask && purpose === 'edit'" v-if="advancedSettingsShowRestoreStreak"
class="option mt-3" class="option mt-3"
> >
<div class="form-group"> <div class="form-group">
@@ -479,8 +478,7 @@
</div> </div>
</div> </div>
<div <div
v-if="task.type === 'habit' v-if="advancedSettingsShowAdjustCounter"
&& isUserTask && purpose === 'edit' && (task.up || task.down)"
class="option mt-3" class="option mt-3"
> >
<div class="form-group"> <div class="form-group">
@@ -539,6 +537,31 @@
</div> </div>
</div> </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> </div>
</b-collapse> </b-collapse>
</div> </div>
@@ -882,6 +905,11 @@
height: 1rem; height: 1rem;
} }
label {
display: inline-flex;
align-items: center;
}
.habit-option { .habit-option {
&-container { &-container {
min-width: 3rem; min-width: 3rem;
@@ -997,7 +1025,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import syncTask from '../../mixins/syncTask'; import syncTask from '../../mixins/syncTask';
import informationIcon from '@/assets/svg/information.svg';
import positiveIcon from '@/assets/svg/positive.svg'; import positiveIcon from '@/assets/svg/positive.svg';
import negativeIcon from '@/assets/svg/negative.svg'; import negativeIcon from '@/assets/svg/negative.svg';
import streakIcon from '@/assets/svg/streak.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 chevronIcon from '@/assets/svg/chevron.svg';
import calendarIcon from '@/assets/svg/calendar.svg'; import calendarIcon from '@/assets/svg/calendar.svg';
import gripIcon from '@/assets/svg/grip.svg'; import gripIcon from '@/assets/svg/grip.svg';
import InformationIcon from '@/components/ui/informationIcon.vue';
export default { export default {
components: { components: {
InformationIcon,
SelectMulti, SelectMulti,
Datepicker, Datepicker,
checklist, checklist,
@@ -1029,7 +1058,6 @@ export default {
showAssignedSelect: false, showAssignedSelect: false,
newChecklistItem: null, newChecklistItem: null,
icons: Object.freeze({ icons: Object.freeze({
information: informationIcon,
negative: negativeIcon, negative: negativeIcon,
positive: positiveIcon, positive: positiveIcon,
destroy: deleteIcon, destroy: deleteIcon,
@@ -1064,25 +1092,25 @@ export default {
dayMapping: 'constants.DAY_MAPPING', dayMapping: 'constants.DAY_MAPPING',
ATTRIBUTES: 'constants.ATTRIBUTES', ATTRIBUTES: 'constants.ATTRIBUTES',
}), }),
advancedSettingsAvailable () { // region advanced settings
if ( advancedSettingsShowAdjustCounter () {
this.task.type === 'reward' return this.task.type === 'habit'
|| this.task.type === 'todo' && this.isUserTask && this.purpose === 'edit'
|| this.purpose === 'create' && (this.task.up || this.task.down);
|| !this.isUserTask
) {
return false;
}
if (this.task.type === 'habit'
&& !this.task.up
&& !this.task.down
) {
return false;
}
return true;
}, },
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 () { checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask; return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
}, },
@@ -1157,7 +1185,6 @@ export default {
}, },
}, },
async mounted () { async mounted () {
this.showAdvancedOptions = !this.user.preferences.advancedCollapsed;
if (this.groupId) { if (this.groupId) {
const groupResponse = await axios.get(`/api/v4/groups/${this.groupId}`); const groupResponse = await axios.get(`/api/v4/groups/${this.groupId}`);
this.managers = Object.keys(groupResponse.data.data.managers); 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" @show="isOpened = true"
@hide="isOpened = false" @hide="isOpened = false"
> >
<template v-slot:button-content> <template #button-content>
<slot <slot
name="item" name="item"
:item="selected || placeholder" :item="selected || placeholder"
@@ -21,13 +21,13 @@
</template> </template>
<b-dropdown-item <b-dropdown-item
v-for="item in items" v-for="item in items"
:key="keyProp ? item[keyProp] : item" :key="getKeyProp(item)"
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]" :disabled="isDisabled(item)"
:active="item === selected" :active="isSelected(item)"
:class="{ :class="{
active: item === selected, active: isSelected(item),
selectListItem: true, selectListItem: true,
showIcon: !hideIcon && item === selected showIcon: !hideIcon && isSelected(item)
}" }"
@click="selectItem(item)" @click="selectItem(item)"
> >
@@ -51,39 +51,39 @@
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~@/assets/scss/colors.scss'; @import '~@/assets/scss/colors.scss';
.select-list ::v-deep { .select-list ::v-deep {
.dropdown-toggle { .dropdown-toggle {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */ 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 { &:not(.showIcon) {
position: relative; .svg-icon.check-icon {
display: none;
.dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
} }
}
&:not(.showIcon) { .svg-icon.check-icon.color {
.svg-icon.check-icon { margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
display: none; 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> </style>
<script> <script>
@@ -101,6 +101,9 @@ export default {
keyProp: { keyProp: {
type: String, type: String,
}, },
activeKeyProp: {
type: String,
},
disabledProp: { disabledProp: {
type: String, type: String,
}, },
@@ -128,10 +131,23 @@ export default {
}; };
}, },
methods: { methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item;
},
isDisabled (item) {
return typeof item[this.disabledProp] === 'undefined' ? false : item[this.disabledProp];
},
selectItem (item) { selectItem (item) {
this.selected = item; this.selected = this.getKeyProp(item);
this.$emit('select', item); this.$emit('select', item);
}, },
isSelected (item) {
if (this.activeKeyProp) {
return item[this.activeKeyProp] === this.selected;
}
return item === this.selected;
},
}, },
}; };
</script> </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 Vue from 'vue';
import BootstrapVue from 'bootstrap-vue'; import BootstrapVue from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app'; import AppComponent from './app';
import { import {
setup as setupAnalytics, setup as setupAnalytics,
@@ -28,6 +29,7 @@ Vue.config.productionTip = IS_PRODUCTION;
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] }); Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
Vue.use(StoreModule); Vue.use(StoreModule);
Vue.use(BootstrapVue); Vue.use(BootstrapVue);
Vue.use(Fragment.Plugin);
setUpLogging(); setUpLogging();
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time 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]; return num.toString().match(re)[0];
} }
export default { export const NotificationMixins = {
computed: { computed: {
...mapState({ notifications: 'notificationStore' }), ...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 => { export const userCustomStateMixin = fieldname => {
const map = { }; const map = { };
map[fieldname] = 'user.data'; map[fieldname] = 'user.data';
return { // eslint-disable-line import/prefer-default-export return {
computed: { computed: {
...mapState(map), ...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> <script>
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import PurchaseHistoryTable from '../ui/purchaseHistoryTable.vue'; import PurchaseHistoryTable from '../../components/ui/purchaseHistoryTable.vue';
export default { export default {
components: { 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: 'logout', path: '/logout', component: Logout },
{ {
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false }, 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 }, { name: 'tasks', path: '/', component: UserTasks },
{ {

View File

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

View File

@@ -65,7 +65,7 @@ export async function sleep (store) {
} }
export async function addWebhook (store, payload) { 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; return response.data.data;
} }

View File

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

View File

@@ -1,7 +1,5 @@
{ {
"frequentlyAskedQuestions": "Frequently Asked Questions", "frequentlyAskedQuestions": "Frequently Asked Questions",
"general": "General",
"faqQuestion0": "I'm confused. Where do I get an overview?", "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.", "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.", "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.", "missingPassword": "Missing password.",
"missingNewPassword": "Missing new password.", "missingNewPassword": "Missing new password.",
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>", "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.", "incorrectDeletePhrase": "Please type <%= magicWord %> in all capital letters to delete your account.",
"notAnEmail": "Invalid email address.", "notAnEmail": "Invalid email address.",
"emailTaken": "Email address is already used in an account.", "emailTaken": "Email address is already used in an account.",

View File

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

View File

@@ -1,18 +1,26 @@
{ {
"settings": "Settings", "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", "language": "Language",
"siteLanguage": "Site Language",
"americanEnglishGovern": "In the event of a discrepancy in the translations, the American English version governs.", "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", "stickyHeader": "Sticky header",
"newTaskEdit": "Open new tasks in edit mode", "newTaskEdit": "Open new tasks in edit mode",
"reverseChatOrder": "Show chat messages in reverse order", "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", "dontShowAgain": "Don't show this again",
"suppressLevelUpModal": "Don't show popup when gaining a level", "showLevelUpModal": "When Gaining a Level",
"suppressHatchPetModal": "Don't show popup when hatching a pet", "showHatchPetModal": "When Hatching a Pet",
"suppressRaisePetModal": "Don't show popup when raising a pet into a mount", "showRaisePetModal": "When Raising a Pet into a Mount",
"suppressStreakModal": "Don't show popup when attaining a Streak achievement", "showStreakModal": "When Attaining a Streak Achievement",
"baileyAnnouncement": "Latest Bailey Announcement",
"view": "View",
"showTour": "Show Tour", "showTour": "Show Tour",
"showBailey": "Show Bailey", "showBailey": "Show Bailey",
"showBaileyPop": "Bring Bailey the Town Crier out of hiding so you can review past news.", "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.", "resetAccPop": "Start over, removing all levels, gold, gear, history, and tasks.",
"deleteAccount": "Delete Account", "deleteAccount": "Delete Account",
"deleteAccPop": "Cancel and remove your Habitica 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", "dataExport": "Data Export",
"saveData": "Here are a few options for saving your data.", "saveData": "Here are a few options for saving your data.",
"habitHistory": "Habit History", "habitHistory": "Habit History",
"exportHistory": "Export History:", "exportHistory": "Export History:",
"csv": "(CSV)", "csv": "(CSV)",
"downloadCSV": "Download CSV",
"downloadAs": "Download as",
"userData": "User Data", "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:", "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", "export": "Export",
"xml": "(XML)", "xml": "XML",
"json": "(JSON)", "json": "JSON",
"api": "API",
"customDayStart": "Custom Day Start", "customDayStart": "Custom Day Start",
"adjustment": "Adjustment", "adjustment": "Adjustment",
"dayStartAdjustment": "Day Start 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.", "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", "misc": "Misc",
"showHeader": "Show Header", "showHeader": "Show Header",
"currentPass": "Current Password",
"changePass": "Change Password", "changePass": "Change Password",
"changeUsername": "Change Username", "changeUsername": "Change Username",
"changeEmail": "Change Email Address", "changeEmail": "Change Email Address",
@@ -54,11 +74,18 @@
"confirmPass": "Confirm New Password", "confirmPass": "Confirm New Password",
"newUsername": "New Username", "newUsername": "New Username",
"dangerZone": "Danger Zone", "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.", "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.",
"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.", "resetDetail1": "You will lose all your levels, Gold, and Experience points.",
"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.", "resetDetail2": "You will keep your current class, achievements and your pets and mounts.",
"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.", "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", "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", "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.", "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!)", "APIToken": "API Token (this is a password - see warning above!)",
@@ -70,13 +97,14 @@
"resetDo": "Do it, reset my account!", "resetDo": "Do it, reset my account!",
"resetComplete": "Reset complete!", "resetComplete": "Reset complete!",
"fixValues": "Fix Values", "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!", "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": "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.", "fixValuesText2": "<b>Note</b>: To restore Streaks on individual Tasks, edit the Task and use the Restore Streak field.",
"fix21Streaks": "21-Day Streaks", "fix21Streaks": "21-Day Streaks",
"discardChanges": "Discard Changes", "discardChanges": "Discard Changes",
"deleteDo": "Do it, delete my account!", "deleteDo": "Do it, delete my account!",
"invalidPasswordResetCode": "The supplied password reset code is invalid or has expired.", "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.", "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", "displayNameSuccess": "Display name successfully changed",
"emailSuccess": "Email successfully changed", "emailSuccess": "Email successfully changed",
"passwordSuccess": "Password successfully changed", "passwordSuccess": "Password successfully changed",
@@ -98,8 +126,8 @@
"giftedSubscriptionInfo": "<%= name %> gifted you a <%= months %> month subscription", "giftedSubscriptionInfo": "<%= name %> gifted you a <%= months %> month subscription",
"giftedSubscriptionFull": "Hello <%= username %>, <%= sender %> has sent you <%= monthCount %> months of 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!", "giftedSubscriptionWinterPromo": "Hello <%= username %>, you received <%= monthCount %> months of subscription as part of our holiday gift-giving promotion!",
"invitedParty": "You were invited to a Party", "invitedParty": "Invited to a Party",
"invitedGuild": "You were invited to a Guild", "invitedGuild": "Invited to a Guild",
"importantAnnouncements": "Reminders to check in to complete tasks and receive prizes", "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!)", "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", "onboarding": "Guidance with setting up your Habitica account",
@@ -107,25 +135,24 @@
"subscriptionReminders": "Subscriptions Reminders", "subscriptionReminders": "Subscriptions Reminders",
"questStarted": "Your Quest has Begun", "questStarted": "Your Quest has Begun",
"invitedQuest": "Invited to Quest", "invitedQuest": "Invited to Quest",
"kickedGroup": "Kicked from group", "kickedGroup": "Removed from group",
"remindersToLogin": "Reminders to check in to Habitica", "remindersToLogin": "Reminders to check in to Habitica",
"unsubscribedSuccessfully": "Unsubscribed successfully!", "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).", "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.", "unsubscribedTextOthers": "You won't receive any other email from Habitica.",
"unsubscribeAllEmails": "Check to Unsubscribe from Emails", "unsubscribeAllEmails": "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.", "unsubscribeAllEmailsText": "Habitica will be unable to notify you via email about important changes to the site or your account.",
"unsubscribeAllPush": "Check to Unsubscribe from all Push Notifications", "unsubscribeAllPush": "Unsubscribe from all Push Notifications",
"correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.", "correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.",
"subscriptionRateText": "Recurring <strong>$<%= price %> USD</strong> every <strong><%= months %> months</strong>", "subscriptionRateText": "Recurring <strong>$<%= price %> USD</strong> every <strong><%= months %> months</strong>",
"giftSubscriptionRateText": "<strong>$<%= price %> USD</strong> for <strong><%= months %> months</strong>", "giftSubscriptionRateText": "<strong>$<%= price %> USD</strong> for <strong><%= months %> months</strong>",
"benefits": "Benefits", "benefits": "Benefits",
"coupon": "Coupon", "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", "apply": "Apply",
"promoCode": "Promo Code", "promoCode": "Promo Code",
"promoCodeApplied": "Promo Code Applied! Check your inventory", "promoCodeApplied": "Promo Code Applied! Check your inventory",
"promoPlaceholder": "Enter Promotion Code", "promoPlaceholder": "Enter Promotion Code",
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
"saveCustomDayStart": "Save Custom Day Start", "saveCustomDayStart": "Save Custom Day Start",
"registration": "Registration", "registration": "Registration",
"addLocalAuth": "Add Email and Password Login", "addLocalAuth": "Add Email and Password Login",
@@ -134,9 +161,10 @@
"generate": "Generate", "generate": "Generate",
"getCodes": "Get Codes", "getCodes": "Get Codes",
"webhooks": "Webhooks", "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", "enabled": "Enabled",
"webhookURL": "Webhook URL", "webhookURL": "Webhook URL",
"addWebhook": "Add Webhook",
"invalidUrl": "invalid url", "invalidUrl": "invalid url",
"invalidWebhookId": "the \"id\" parameter should be a valid UUID.", "invalidWebhookId": "the \"id\" parameter should be a valid UUID.",
"webhookBooleanOption": "\"<%= option %>\" must be a Boolean value.", "webhookBooleanOption": "\"<%= option %>\" must be a Boolean value.",
@@ -179,7 +207,14 @@
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!", "usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
"usernameNotVerified": "Please confirm your username.", "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.", "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!", "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", "mentioning": "Mentioning",
"suggestMyUsername": "Suggest my username", "suggestMyUsername": "Suggest my username",
"everywhere": "Everywhere", "everywhere": "Everywhere",
@@ -189,6 +224,11 @@
"amount": "Amount", "amount": "Amount",
"action": "Action", "action": "Action",
"note": "Note", "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", "remainingBalance": "Remaining Balance",
"transactions": "Transactions", "transactions": "Transactions",
"hourglassTransactions": "Hourglass Transactions", "hourglassTransactions": "Hourglass Transactions",
@@ -203,7 +243,6 @@
"transaction_gift_receive": "<b>Received</b> from", "transaction_gift_receive": "<b>Received</b> from",
"transaction_create_challenge": "<b>Created</b> challenge", "transaction_create_challenge": "<b>Created</b> challenge",
"transaction_create_bank_challenge": "<b>Created</b> bank 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_create_guild": "<b>Created</b> guild",
"transaction_change_class": "<b>Class</b> change", "transaction_change_class": "<b>Class</b> change",
"transaction_rebirth": "Used Orb of Rebirth", "transaction_rebirth": "Used Orb of Rebirth",
@@ -212,5 +251,8 @@
"transaction_reroll": "Used Fortify Potion", "transaction_reroll": "Used Fortify Potion",
"transaction_subscription_perks": "<b>Subscription</b> perk", "transaction_subscription_perks": "<b>Subscription</b> perk",
"transaction_admin_update_balance": "<b>Admin</b> given", "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!", "streakCoins": "Streak Bonus!",
"taskToTop": "To top", "taskToTop": "To top",
"taskToBottom": "To bottom", "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.", "taskAliasAlreadyUsed": "Task alias already used on another task.",
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".", "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".", "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 // @TODO use those constants to replace hard-coded numbers
export const SUPPORTED_SOCIAL_NETWORKS = [ export const SUPPORTED_SOCIAL_NETWORKS = [
{ key: 'google', name: 'Google' },
{ key: 'apple', name: 'Apple' }, { key: 'apple', name: 'Apple' },
{ key: 'google', name: 'Google' },
]; ];
export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination 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'; // sorting this also changes the class selection
export const CURRENT_SEASON = moment().isBefore('2020-08-02') ? 'summer' : '_NONE_';
export const CLASSES = [ export const CLASSES = [
'warrior',
'rogue',
'healer', 'healer',
'wizard', 'wizard',
'rogue',
'warrior',
]; ];
export const GEAR_TYPES = [ export const GEAR_TYPES = [

View File

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

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