diff --git a/.eslintignore b/.eslintignore index e15880cb6..e8f73169c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -4,3 +4,4 @@ public/missing-locales public/images/custom public/images/uicons server/src/configs/ +packages/types/**/*.d.ts diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2c8cb20e9..488c6c574 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -24,10 +24,10 @@ jobs: run: yarn masterfile - name: Lint - run: yarn eslint:check + run: yarn eslint - name: Prettier - run: yarn prettier:check + run: yarn prettier - name: Build run: yarn build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..acbdcb6df --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,37 @@ +name: GitHub Release + +on: + pull_request: + push: + branches: + - main + - develop + +permissions: write-all + +jobs: + release: + name: Build & release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Install Volta + uses: volta-cli/action@v4 + - name: Echo versions + run: | + node --version + yarn --version + - name: Install dependencies + run: yarn --prefer-offline + env: + HUSKY: 0 + - name: Build app + run: yarn build + - name: Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HUSKY: 0 + run: npx semantic-release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e0b793c4..5cac87c9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,9 +19,6 @@ jobs: - name: Install Dependencies run: yarn - - name: Create Locales - run: yarn create-locales - - name: Sentry Build run: yarn release env: diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..fc28b52c8 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no -- commitlint --edit "${1}" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..ba85fbeec --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged --verbose diff --git a/.vscode/settings.json b/.vscode/settings.json index a5abe4d60..edcdb9f37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,8 @@ { "i18n-ally.localesPaths": [ "packages/locales/lib/human", - "packages/locales/lib/generated" + "packages/locales/lib/generated", + "packages/locales/lib/data" ], "i18n-ally.keystyle": "flat", "[javascript]": { @@ -14,4 +15,4 @@ }, "editor.formatOnSave": true, "docwriter.style": "JSDoc" -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..091b77cd7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,172 @@ +# [1.28.0-develop.9](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.8...v1.28.0-develop.9) (2024-01-12) + + +### Bug Fixes + +* `this.refetch` check ([e57f1b0](https://github.com/WatWowMap/ReactMap/commit/e57f1b0235401a3dd90725003fb94b11c33869d3)) + +# [1.28.0-develop.8](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.7...v1.28.0-develop.8) (2024-01-12) + + +### Bug Fixes + +* xxs/xxl names ([d6a8906](https://github.com/WatWowMap/ReactMap/commit/d6a890621a84242264ae35b79e204c5fc9e2bc96)) + +# [1.28.0-develop.7](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.6...v1.28.0-develop.7) (2024-01-12) + + +### Bug Fixes + +* updating robust timeout refetch fn ([ff84870](https://github.com/WatWowMap/ReactMap/commit/ff84870c994136a15da7af2fe31bf947eb830e63)) + +# [1.28.0-develop.6](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.5...v1.28.0-develop.6) (2024-01-11) + + +### Bug Fixes + +* active weather ([6541aa8](https://github.com/WatWowMap/ReactMap/commit/6541aa852a6747912994f733fbe613d355c0b2ab)) +* adjust expert description ([d6f04e0](https://github.com/WatWowMap/ReactMap/commit/d6f04e06918342f4820c6f44d7b9cc6a5ec9595c)) +* admin items ([7e6a62e](https://github.com/WatWowMap/ReactMap/commit/7e6a62e678a384359374efcc751158b99d8485eb)) +* adv filter options on mobile ([93234c3](https://github.com/WatWowMap/ReactMap/commit/93234c37193f612d856153e5d8ab9f2348b84b82)) +* alignment uicons by id ([b17753f](https://github.com/WatWowMap/ReactMap/commit/b17753f8ac8ffaddbbf4fdb9ab9041dfd6159002)) +* available for other permissions ([425ab3a](https://github.com/WatWowMap/ReactMap/commit/425ab3a8f614fe46e28c333f422f2f1bc17efb7a)) +* better slide/input behaviors ([98686b8](https://github.com/WatWowMap/ReactMap/commit/98686b8645d20115953904f270c6f741aa390f54)) +* cleanup from slot selection changes ([57ae335](https://github.com/WatWowMap/ReactMap/commit/57ae33519f0e692e3f1c48bcc527d7496b1efdad)) +* cleanups ([52690be](https://github.com/WatWowMap/ReactMap/commit/52690be10a3accae108c02803c51b14cee2b0111)) +* cleanups ([9ac88ca](https://github.com/WatWowMap/ReactMap/commit/9ac88ca6c18019eae2f7d0f5869ddacfab2719fd)) +* couple of fixes ([ac2a1c5](https://github.com/WatWowMap/ReactMap/commit/ac2a1c536f9d473c0ba27e96246b1a66a9b8fb74)) +* disable prop for multiselector ([1cf4955](https://github.com/WatWowMap/ReactMap/commit/1cf4955c843253e9429155a3ba3a785808f40160)) +* disable prop for size & gender ([33c6df7](https://github.com/WatWowMap/ReactMap/commit/33c6df7204ea8bd7655fec70640ebe423d84c579)) +* ditto filtering ([2f6ddaa](https://github.com/WatWowMap/ReactMap/commit/2f6ddaab3e1c1fe973446a7a326e1911998d884a)) +* dont show showcase mon if null ([e49b885](https://github.com/WatWowMap/ReactMap/commit/e49b885e277a6c6ffbab513c35f16cdfa9fe4c47)) +* enabled/all/disabled logic ([fc6c8ac](https://github.com/WatWowMap/ReactMap/commit/fc6c8ac7e82015d5f3d018814439fb0b1cd6f8d0)) +* ez mode checks ([e2bb1f0](https://github.com/WatWowMap/ReactMap/commit/e2bb1f0d118ce3751cd5209be74f98ea920e9800)) +* ez mode filter behavior ([0b0dd7d](https://github.com/WatWowMap/ReactMap/commit/0b0dd7dee74e1d1602514ec673c48ca646ec408d)) +* filtering edge cases ([a1b1580](https://github.com/WatWowMap/ReactMap/commit/a1b15806af30314dd90712f10180508ab75f7ebd)) +* further dnf refinements ([da08236](https://github.com/WatWowMap/ReactMap/commit/da08236f869be56e1a21f4b8a108f78be1cfa834)) +* global 0/100 iv toggles ([272d80d](https://github.com/WatWowMap/ReactMap/commit/272d80df4c07d858cfb29b3ceef5ad6472c12216)) +* gym badge menu from popup ([8cea2fb](https://github.com/WatWowMap/ReactMap/commit/8cea2fb47d786f1a74dcb03087f13cacb0645dfa)) +* hide all logic in adv menu for some items ([0561829](https://github.com/WatWowMap/ReactMap/commit/05618292589dc60118eaa9f2f3cf90234722c475)) +* hmm ([58d1ece](https://github.com/WatWowMap/ReactMap/commit/58d1ece97ddf4098b1217e2101d2b4b7dbb93237)) +* invasion quick selector ([ad4471a](https://github.com/WatWowMap/ReactMap/commit/ad4471a67ccb88020757c899fcf8ab5e266b9ffc)) +* make the selector more generic friendly for other categories ([6259bc2](https://github.com/WatWowMap/ReactMap/commit/6259bc2101029c367300ebe98df082ad7c15a124)) +* missing label on search ([36f2bd8](https://github.com/WatWowMap/ReactMap/commit/36f2bd8662a12a5c4260e4d144e897acf86571b8)) +* missing translations ([4d6d5ec](https://github.com/WatWowMap/ReactMap/commit/4d6d5eccf28f35794e7d660d0dd25f9475cb36e0)) +* more filter tweaks ([a4c4eee](https://github.com/WatWowMap/ReactMap/commit/a4c4eee7cba6dee004af0ab31ccba84a52273d17)) +* only show catchable shadow pokes ([fd519de](https://github.com/WatWowMap/ReactMap/commit/fd519de58b1cb3ad5f436387851bd3dbea5d7981)) +* only show edit adv for all for pokemon ([622f179](https://github.com/WatWowMap/ReactMap/commit/622f17933dfed9a9b78647a4fcc7c4df3b105fdd)) +* opening adv mode from drawer ([5c2f080](https://github.com/WatWowMap/ReactMap/commit/5c2f080dda691b3f80572ad21e2757117e9c3855)) +* pokemon zoom ([9bbc70d](https://github.com/WatWowMap/ReactMap/commit/9bbc70d54bd21763b00e4cd396ab6669a592a198)) +* poracle manager with new state fixes ([f4564ef](https://github.com/WatWowMap/ReactMap/commit/f4564efe7ca578599756ddead77b957ad28e085f)) +* possible fix for leaflet location issue ([4aefb90](https://github.com/WatWowMap/ReactMap/commit/4aefb90d85db6a1e873355e032c03281b7b13b56)) +* prop cleanups ([8b7d25d](https://github.com/WatWowMap/ReactMap/commit/8b7d25d0f8a862ec50d1a91675fa67c5a7f7364a)) +* pvp mons with no iv perm ([fce7eb9](https://github.com/WatWowMap/ReactMap/commit/fce7eb91e6cb1ed6ee387807b27de8584dafbb14)) +* refine dnf filter generation ([875b961](https://github.com/WatWowMap/ReactMap/commit/875b961dd9bc4cb17ab297f8e2da517e21f04baa)) +* remove showcase on kecleon ([c42c9f4](https://github.com/WatWowMap/ReactMap/commit/c42c9f486ac1cab75cc34e93805fed746aa22f47)) +* remove unnecessary r filters from quick select ([f0a3b28](https://github.com/WatWowMap/ReactMap/commit/f0a3b2853bbe5970cd84917fece6980f2828022a)) +* s2 cells ([2048b73](https://github.com/WatWowMap/ReactMap/commit/2048b73961f740e13affe66bc5291dedde6cf8f8)) +* search case sensitive in adv menu ([be5846d](https://github.com/WatWowMap/ReactMap/commit/be5846d087ecec3af3e44c61297e1b2cab5a3179)) +* search fields ([581949c](https://github.com/WatWowMap/ReactMap/commit/581949c686a47abd9c8f50c78e027ab80bc354f7)) +* search value clear ([05ae312](https://github.com/WatWowMap/ReactMap/commit/05ae312c9db7cf2c642940c2c5cc48adb2a9e039)) +* search/setting filtered behavior ([a30c286](https://github.com/WatWowMap/ReactMap/commit/a30c2869a5642cc4061970e4ef6b1fcbb1bc9fff)) +* slider states when disabled ([4d88c1a](https://github.com/WatWowMap/ReactMap/commit/4d88c1a04ca9b3babbb88093327df3cc8ad4a9be)) +* slight optimization for query requests ([706581b](https://github.com/WatWowMap/ReactMap/commit/706581b28a29c2a09982051c53f29c862c045252)) +* some pokestop filtering ([ef6be7f](https://github.com/WatWowMap/ReactMap/commit/ef6be7f96422b136e846c3a9d53cd30e573b0c6f)) +* some state bugs ([2daf109](https://github.com/WatWowMap/ReactMap/commit/2daf1091019e8c0f757df4b466c7db099ed1fc98)) +* standard filter backup ([5a0cdf5](https://github.com/WatWowMap/ReactMap/commit/5a0cdf5150e9e168898f1da6536d613f45d38350)) +* state bug & consolidate translations ([d6e6a79](https://github.com/WatWowMap/ReactMap/commit/d6e6a79cc5472f773fd632a2b0fec47d4a7bd337)) +* state filter stuff ([70c958e](https://github.com/WatWowMap/ReactMap/commit/70c958ec97b8e8875c4850720775fd164e2614e6)) +* string fallback ([11ce4ca](https://github.com/WatWowMap/ReactMap/commit/11ce4ca61094ebcb24a7457c97c79349c72513b6)) +* submission cell menu items ([01c6350](https://github.com/WatWowMap/ReactMap/commit/01c6350a2fffc1b810d8d5d949d7906844cf3b5c)) +* support for `all` in pokestop model ([cb57574](https://github.com/WatWowMap/ReactMap/commit/cb57574ad8502175ee7b18fd159eb3ce765c5833)) +* top level state changes ([f401bbe](https://github.com/WatWowMap/ReactMap/commit/f401bbe457ecfbb07b42554528c42fad55bd7d75)) +* tut welcome page ([3c9e40c](https://github.com/WatWowMap/ReactMap/commit/3c9e40c7300d06420a83fb0e7292c14c79e050b0)) +* unmount scanAreas section ([49ec133](https://github.com/WatWowMap/ReactMap/commit/49ec1334f217c49532cd85cb4bce5d496fd5dec7)) +* user fields ([15ad84d](https://github.com/WatWowMap/ReactMap/commit/15ad84df882d363a55ccd50fec17ee4372bdb837)) +* vite prod build ([ed93946](https://github.com/WatWowMap/ReactMap/commit/ed93946c653e391210ae6a34f3b968437e249fef)) + + +### Features + +* add searching ([356c01b](https://github.com/WatWowMap/ReactMap/commit/356c01bb4d0754548e4dbb54dfdb9eaef6833a67)) +* dev settings in sidebar ([6a2651f](https://github.com/WatWowMap/ReactMap/commit/6a2651fcc8ef36066efcda1b5e9be72993c212da)) +* gym quick selector ([93c8fab](https://github.com/WatWowMap/ReactMap/commit/93c8fab22f7b6554ba6e03cc9e1c3411c70877a5)) +* invasions quick select ([c638225](https://github.com/WatWowMap/ReactMap/commit/c63822561add83b6067cad620d654ba615871a30)) +* literal easy mode ([5298676](https://github.com/WatWowMap/ReactMap/commit/52986760bc537ced305b8d43dca91c124da21076)) +* lure quick selector ([98ee717](https://github.com/WatWowMap/ReactMap/commit/98ee7178129c594f335aaa143bc3b5ca4d8be63a)) +* nest quick select menu ([37761d7](https://github.com/WatWowMap/ReactMap/commit/37761d7b46e803b1d33f81319108a6dec785f91e)) +* pokemon filter mode selector ([be846fc](https://github.com/WatWowMap/ReactMap/commit/be846fcbf9203fa5ae53557fa67f093eeba13f9e)) +* quest quick select menu ([02d8d65](https://github.com/WatWowMap/ReactMap/commit/02d8d65719576ce3992019fa0751e4dd56813a3b)) +* quick select pkmn filters ([ee1010b](https://github.com/WatWowMap/ReactMap/commit/ee1010bde398e3fc8d8c7a7d6983d6484fda7221)) +* raids quick select menu ([dc3e10a](https://github.com/WatWowMap/ReactMap/commit/dc3e10a07fd4da042d434dc9a915dd77583d2629)) +* respect global if no pkmn are active in basic mode ([c96c99f](https://github.com/WatWowMap/ReactMap/commit/c96c99fd85e7d426b41675e9494d5e4850594109)) +* show event stop markers if no quests/invasions ([32df5e9](https://github.com/WatWowMap/ReactMap/commit/32df5e9bcfc5180a6afa4b5a14b6b69d7a1237b6)) +* showcase quick select ([1ae5fab](https://github.com/WatWowMap/ReactMap/commit/1ae5fab836d1bf7f70495541088a7685cdb9b859)) +* tooltip ([a3777dd](https://github.com/WatWowMap/ReactMap/commit/a3777dd04bd371c7464525113a5ebf64e619c7d6)) +* useTranslateById AIO hook for ez key translations ([20dd65d](https://github.com/WatWowMap/ReactMap/commit/20dd65d67c51d3310ffac462184d0ac59fb5095d)) + +# [1.28.0-develop.5](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.4...v1.28.0-develop.5) (2024-01-07) + + +### Bug Fixes + +* make `allForms` false by default for usability ([cff0e23](https://github.com/WatWowMap/ReactMap/commit/cff0e234a1d9a4069da8a4cd12b5f8a0d3f9c6a0)) + +# [1.28.0-develop.4](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.3...v1.28.0-develop.4) (2024-01-07) + + +### Bug Fixes + +* subcategory viewing ([3ed2e8f](https://github.com/WatWowMap/ReactMap/commit/3ed2e8f53d59913138d23eb117a5340316866df1)) + +# [1.28.0-develop.3](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.2...v1.28.0-develop.3) (2024-01-06) + + +### Bug Fixes + +* rockruff form name issues ([188c63a](https://github.com/WatWowMap/ReactMap/commit/188c63a7e61e96f807458981e822f646c1c037d8)) + +# [1.28.0-develop.2](https://github.com/WatWowMap/ReactMap/compare/v1.28.0-develop.1...v1.28.0-develop.2) (2024-01-03) + + +### Bug Fixes + +* motd index setter ([f68153a](https://github.com/WatWowMap/ReactMap/commit/f68153a141ec038d48e37945772bb8da85f26a28)) +* motd types ([c4ad276](https://github.com/WatWowMap/ReactMap/commit/c4ad276f303357ac2c9b3698b7c6a07314d339c1)) + +# [1.28.0-develop.1](https://github.com/WatWowMap/ReactMap/compare/v1.27.2...v1.28.0-develop.1) (2024-01-02) + + +### Bug Fixes + +* `Notification` API checks ([22bb9ea](https://github.com/WatWowMap/ReactMap/commit/22bb9eab2e5da0ec0780206fd059d1cfe8f5c666)) +* active scanNext/scanZone btns ([5d5f8bb](https://github.com/WatWowMap/ReactMap/commit/5d5f8bbc9f1f419a75b32dcf6bf654da9836c13c)) +* add permission status to notif options menu ([c44a75c](https://github.com/WatWowMap/ReactMap/commit/c44a75c7aa5cfff504782aabe1bc102a5a6bdfa3)) +* also if map perm is missing ([6abc14e](https://github.com/WatWowMap/ReactMap/commit/6abc14e95ba80f09a26873231c4e79d2ea13782b)) +* catch bad play attempts ([5c68483](https://github.com/WatWowMap/ReactMap/commit/5c6848383111b06a7d188dacec5786112c795f84)) +* category toggles ([151ce50](https://github.com/WatWowMap/ReactMap/commit/151ce50f212479fa1569f5586a7faac34d7e652f)) +* ci stuff ([f960d91](https://github.com/WatWowMap/ReactMap/commit/f960d91c13a0863f6bcee5c2d9400e478473593a)) +* code organization ([7ced12e](https://github.com/WatWowMap/ReactMap/commit/7ced12e3d9c8762ceb410f0e6f01be8ec60939a6)) +* force popup open on click ([7c9f045](https://github.com/WatWowMap/ReactMap/commit/7c9f0454dbdeb9c62f44cc0861571e43127cb9ad)) +* generalize fallback ext ([8979d92](https://github.com/WatWowMap/ReactMap/commit/8979d92133a42145bb4fdb60f009a95fa3768811)) +* icon user settings ([4381817](https://github.com/WatWowMap/ReactMap/commit/43818171a385e6882f68f6c62365e124a6df793e)) +* let it be configurable too ([268f7d8](https://github.com/WatWowMap/ReactMap/commit/268f7d811fbb3e5b1ca0c5baa18b14db5bf096aa)) +* locales ([6a4ba6b](https://github.com/WatWowMap/ReactMap/commit/6a4ba6bb04fe07cc716f0d4aa4325f8c0e449390)) +* make sure window is focused on click ([c18f746](https://github.com/WatWowMap/ReactMap/commit/c18f746e93190ae12d56aea8b0ff5032059938e3)) +* missing join translation ([e532995](https://github.com/WatWowMap/ReactMap/commit/e532995285012313c42adab7904037dc84a3161e)) +* only show if customizable length > 0 ([0b6c89c](https://github.com/WatWowMap/ReactMap/commit/0b6c89cae2d926652e80db28d51c0666c1949202)) +* prompt user on options open ([f2c40fd](https://github.com/WatWowMap/ReactMap/commit/f2c40fdb89f17955150061e2093630bf85dd7e0a)) +* return early if in cache ([238edbd](https://github.com/WatWowMap/ReactMap/commit/238edbdc8231856d78dec503d0f6c366fd67ede1)) +* tilelayer background ([4ad64ca](https://github.com/WatWowMap/ReactMap/commit/4ad64cad5a571c27edcd1c687783fdd7560ac7e6)) +* volta versions ([ba829db](https://github.com/WatWowMap/ReactMap/commit/ba829dbd2726db5ebdf115cacdcb33b0a6a22fa9)) + + +### Features + +* base notification implementation ([42de1e9](https://github.com/WatWowMap/ReactMap/commit/42de1e927f71757ea8a242185d8c1edc6cbb02bb)) +* ci for github releases ([4015be6](https://github.com/WatWowMap/ReactMap/commit/4015be68dcc6be7c248e1e93ccb3973e433b7ae9)) +* commit hooks ([945c572](https://github.com/WatWowMap/ReactMap/commit/945c572bee182c9c3ea7352d5320c22e9bcc06e5)) +* pokemon notifs ([adf7429](https://github.com/WatWowMap/ReactMap/commit/adf74293d3fcefed63e851a75a95e04151f8ec32)) +* raids ([9fc1b04](https://github.com/WatWowMap/ReactMap/commit/9fc1b0487aff1d7a6eea66e22eb8fa043df374d0)) +* show signed in info when blocked ([59b20cb](https://github.com/WatWowMap/ReactMap/commit/59b20cbcc7ef4254b803f66bb36a9fbe988c92ad)) +* support for uaudio ([1f30c7c](https://github.com/WatWowMap/ReactMap/commit/1f30c7c0aa4acf061849ec059d1b1db1ad9b302a)) diff --git a/package.json b/package.json index 75a9b72eb..c367b9ad2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactmap", - "version": "1.28.0", + "version": "1.28.0-develop.9", "private": true, "description": "React based frontend map.", "license": "MIT", @@ -14,8 +14,8 @@ "config:check": "yarn workspace @rm/config run check", "config:env": "yarn workspace @rm/config run generate", "dev": "NODE_ENV=development nodemon server/src/index.js", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", "locales:create": "yarn workspace @rm/locales run create", "locales:generate": "yarn workspace @rm/locales run generate", "locales:missing": "yarn workspace @rm/locales run missing", @@ -23,11 +23,12 @@ "migrate:latest": "knex --knexfile server/src/db/knexfile.cjs migrate:latest", "migrate:make": "knex --knexfile server/src/db/knexfile.cjs migrate:make", "migrate:rollback": "knex --knexfile server/src/db/knexfile.cjs migrate:rollback", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"", "release": "vite build -- -r", "server": "node server/src/index.js", "start": "node .", + "prepare": "husky install", "watch": "vite" }, "nodemonConfig": { @@ -42,9 +43,65 @@ "logs" ] }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "eslint --fix" + ], + "**/*": [ + "prettier --write --ignore-unknown" + ] + }, + "config": { + "commitizen": { + "path": "cz-conventional-changelog" + } + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release": { + "branches": [ + "+([0-9])?(.{+([0-9]),x}).x", + "main", + { + "name": "develop", + "prerelease": true + } + ], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + [ + "@semantic-release/npm", + { + "npmPublish": false + } + ], + "@semantic-release/github", + [ + "@semantic-release/git", + { + "assets": [ + "CHANGELOG.md", + "package.json", + "yarn.lock" + ], + "message": "chore(release): ${nextRelease.gitTag} [skip ci]\n\n${nextRelease.notes}" + } + ] + ], + "preset": "angular" + }, + "volta": { + "node": "18.18.2", + "yarn": "1.22.19" + }, "dependencies": { - "@apollo/client": "^3.8.7", - "@apollo/server": "^4.9.5", + "@apollo/client": "^3.8.9", + "@apollo/server": "^4.10.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@graphql-tools/graphql-file-loader": "^8.0.0", @@ -91,7 +148,7 @@ "i18next-http-backend": "^1.4.0", "knex": "^2.4.2", "leaflet": "1.9.4", - "leaflet.locatecontrol": "^0.73.0", + "leaflet.locatecontrol": "^0.79.0", "lodash.debounce": "^4.0.8", "moment-timezone": "^0.5.43", "morgan": "^1.10.0", @@ -111,9 +168,7 @@ "react-i18next": "^11.16.7", "react-leaflet": "4.2.1", "react-router-dom": "^6.15.0", - "react-virtualized-auto-sizer": "^1.0.20", - "react-virtuoso": "^4.5.0", - "react-window": "^1.8.9", + "react-virtuoso": "^4.6.2", "rtree": "^1.4.2", "source-map": "^0.7.4", "suncalc": "^1.9.0", @@ -121,13 +176,19 @@ "zustand": "4.4.6" }, "devDependencies": { + "@commitlint/cli": "^17.2.0", + "@commitlint/config-conventional": "^17.2.0", "@rm/types": "*", + "@semantic-release/changelog": "^6.0.1", + "@semantic-release/git": "^10.0.1", "@sentry/vite-plugin": "2.2.1", "@types/dlv": "^1.1.2", "@types/node": "^18", "@types/react": "^18.2.20", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "4.0.0", + "commitizen": "^4.2.5", + "cz-conventional-commit": "^1.0.6", "eslint": "^8.44.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-prettier": "^8.8.0", @@ -136,10 +197,13 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "husky": "^8.0.1", + "lint-staged": "^13.0.3", "monaco-editor": "^0.41.0", "nodemon": "^3.0.2", "prettier": "^2.8.8", "rollup-plugin-delete": "^2.0.0", + "semantic-release": "^19.0.5", "vite": "4.3.9", "vite-plugin-checker": "0.6.0", "vite-plugin-static-copy": "0.16.0" @@ -148,4 +212,4 @@ "node": ">=18", "yarn": "^1.22.x" } -} \ No newline at end of file +} diff --git a/packages/config/.configref b/packages/config/.configref index 1c231b107..7681d0e29 100644 --- a/packages/config/.configref +++ b/packages/config/.configref @@ -1 +1 @@ -23527 \ No newline at end of file +23681 \ No newline at end of file diff --git a/packages/config/package.json b/packages/config/package.json index 30add5495..05338c042 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -9,12 +9,12 @@ "check": "node ./lib/scripts/configCheck.js", "sort": "npx sort-package-json", "depCheck": "npx depcheck", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "dependencies": { "config": "^3.3.9" } -} \ No newline at end of file +} diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index c28f3dc47..84c8612cd 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -407,7 +407,7 @@ "all_gyms": "All Gyms", "general": "General", "server_dev_error_0": "{{variable_0}}", - "link_global_and_advanced": "Link Global & Advanced", + "link_global_and_advanced": "Global Respects Selected", "normal_forms": "Normal Forms", "weather_indicator": "Weather Boost Indicator", "page": "Page {{page}}", @@ -701,5 +701,24 @@ "audio_always_on": "Audio Always Plays", "volume_level": "Volume Level", "notifications_status": "Notifications Status", - "granted": "granted" + "granted": "granted", + "set_all": "Set All", + "set_filtered": "Set Filtered", + "only_show_available": "Only Show Available", + "easy_mode": "Easy Mode", + "quick_select": "Global Quick Selects", + "pokemon_filter_mode": "Pokémon Filter Mode", + "basic": "Basic", + "intermediate": "Intermediate", + "expert": "Expert", + "basic_description": "Easily select Pokémon and apply a global filter", + "intermediate_description": "Set individual filters globally and per Pokémon (traditional)", + "expert_description": "Manual input queries for the most customization", + "icon_size": "Icon Size", + "developer": "Developer", + "raid_override": "Raid Override", + "search_rocket_pokemon": "Search Rocket Pokémon", + "main": "Main", + "extra": "Extra", + "select": "Select" } diff --git a/packages/locales/package.json b/packages/locales/package.json index 4aad4c7d4..794e288c9 100644 --- a/packages/locales/package.json +++ b/packages/locales/package.json @@ -9,9 +9,9 @@ "missing": "node ./lib/missing.js", "sort": "npx sort-package-json", "depCheck": "npx depcheck", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "dependencies": { @@ -20,4 +20,4 @@ "dotenv": "^16.3.1", "openai": "^4.24.1" } -} \ No newline at end of file +} diff --git a/packages/logger/package.json b/packages/logger/package.json index 49d14fcb4..f5237ff86 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -6,9 +6,9 @@ "scripts": { "sort": "npx sort-package-json", "depCheck": "npx depcheck", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "dependencies": { @@ -16,4 +16,4 @@ "chalk": "^4", "loglevel": "^1.8.1" } -} \ No newline at end of file +} diff --git a/packages/masterfile/package.json b/packages/masterfile/package.json index 64daf919a..f48d18381 100644 --- a/packages/masterfile/package.json +++ b/packages/masterfile/package.json @@ -7,9 +7,9 @@ "generate": "node ./lib", "sort": "npx sort-package-json", "depCheck": "npx depcheck", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "dependencies": { @@ -17,4 +17,4 @@ "@rm/logger": "*", "@rm/types": "*" } -} \ No newline at end of file +} diff --git a/packages/types/lib/client.d.ts b/packages/types/lib/client.d.ts index eb486bb11..b1a455e5f 100644 --- a/packages/types/lib/client.d.ts +++ b/packages/types/lib/client.d.ts @@ -1,7 +1,8 @@ import * as React from 'react' import { Config } from './config' import UAssets from '@services/Icons' -import { ButtonProps } from '@mui/material' +import { ButtonProps, SxProps, Theme } from '@mui/material' +import { SystemStyleObject } from '@mui/system' declare global { declare const CONFIG: Config @@ -15,3 +16,38 @@ declare global { export interface CustomI extends React.HTMLProps { size?: ButtonProps['size'] } + +export type TimesOfDay = 'day' | 'night' | 'dawn' | 'dusk' + +export type MarginProps = { + [Key in + | 'm' + | 'mt' + | 'mb' + | 'ml' + | 'mr' + | 'mx' + | 'my']?: React.CSSProperties['margin'] +} + +export type PaddingProps = { + [Key in + | 'p' + | 'pt' + | 'pb' + | 'pl' + | 'pr' + | 'px' + | 'py']?: React.CSSProperties['padding'] +} + +export interface MultiSelectorProps { + value: V + items: readonly V[] + tKey?: string + disabled?: boolean + onClick?: ( + oldValue: V, + newValue: V, + ) => (e?: React.MouseEvent) => void +} diff --git a/packages/types/lib/config.d.ts b/packages/types/lib/config.d.ts index c56053a79..197dd4974 100644 --- a/packages/types/lib/config.d.ts +++ b/packages/types/lib/config.d.ts @@ -3,6 +3,7 @@ import config = require('server/src/configs/default.json') import example = require('server/src/configs/local.example.json') import type { Schema } from './server' +import type { DialogProps } from '@mui/material' type BaseConfig = typeof config type ExampleConfig = typeof example @@ -74,13 +75,14 @@ export interface Config messageOfTheDay: { settings: { parentStyle: Record // should be CSS properties but performance seems to die - } & BaseConfig['map']['messageOfTheDay']['settings'] + } & Omit titles: string[] components: CustomComponent[] footerButtons: CustomComponent[] + dialogMaxWidth: DialogProps['maxWidth'] } & Omit< BaseConfig['map']['messageOfTheDay'], - 'settings' | 'titles' | 'components' | 'footerButtons' + 'settings' | 'titles' | 'components' | 'footerButtons' | 'dialogMaxWidth' > donationPage: { settings: { @@ -105,8 +107,8 @@ export interface Config database: { schemas: Schema[] settings: { - extraUserFields: string[] - } & BaseConfig['database']['settings'] + extraUserFields: (ExtraField | string)[] + } & Omit } & BaseConfig['database'] scanner: { scanNext: { @@ -128,6 +130,12 @@ export interface Config manualAreas: ExampleConfig['manualAreas'][number][] } +export interface ExtraField { + name: string + database: string + disabled: boolean +} + export interface Webhook { enabled: boolean provider: 'poracle' @@ -145,6 +153,7 @@ export interface Webhook { } export interface CustomComponent { + type?: string components?: CustomComponent[] donorOnly?: boolean freeloaderOnly?: boolean @@ -160,7 +169,7 @@ export type DeepKeys = { : never }[keyof T] -export type ConfigPaths = DeepKeys +export type ConfigPaths = DeepKeys export type PathValue = P extends `${infer K}.${infer Rest}` ? K extends keyof T @@ -172,7 +181,10 @@ export type PathValue = P extends `${infer K}.${infer Rest}` ? T[P] : never -export type ConfigPathValue

= PathValue +export type ConfigPathValue< + T extends object, + P extends ConfigPaths, +> = PathValue export type Join = K extends string | number ? P extends string | number @@ -220,4 +232,4 @@ export type NestedObjectPaths = Paths export type GetSafeConfig =

( path: P, -) => ConfigPathValue

+) => ConfigPathValue diff --git a/packages/types/lib/general.d.ts b/packages/types/lib/general.d.ts index 9425279f6..0ba326875 100644 --- a/packages/types/lib/general.d.ts +++ b/packages/types/lib/general.d.ts @@ -26,6 +26,7 @@ export type RMGeoJSON = { import masterfile = require('packages/masterfile/lib/data/masterfile.json') import { Config } from './config' +import { SliderProps } from '@mui/material' export type Masterfile = typeof masterfile @@ -99,3 +100,28 @@ export type UAssetsClient = Config['icons']['styles'][number] & { data: UICONS } export type FullClientIcons = Omit & { styles: (Config['icons']['styles'][number] & { data: UICONS })[] } + +export interface RMSlider extends SliderProps { + label?: string + perm?: string + step?: number + i18nKey?: string + disabled?: boolean + low?: number + high?: number + i18nKey?: string + markI18n?: string + noTextInput?: boolean + marks?: number[] +} + +export type RMSliderHandleChange = ( + name: N, + values: number | number[], +) => void + +export interface RMSliderProps { + slide: RMSlider + values: number[] + handleChange: RMSliderHandleChange +} diff --git a/packages/types/lib/scanner.d.ts b/packages/types/lib/scanner.d.ts index cd94a4c82..69df57c87 100644 --- a/packages/types/lib/scanner.d.ts +++ b/packages/types/lib/scanner.d.ts @@ -62,6 +62,8 @@ export interface Gym { power_up_level: number power_up_points: number power_up_end_timestamp: number + deleted: boolean + enabled: boolean } export type FullGym = FullModel diff --git a/packages/types/lib/server.d.ts b/packages/types/lib/server.d.ts index ad9109077..fe94cae4a 100644 --- a/packages/types/lib/server.d.ts +++ b/packages/types/lib/server.d.ts @@ -21,7 +21,7 @@ import Backup = require('server/src/models/Backup') import Nest = require('server/src/models/Nest') import NestSubmission = require('server/src/models/NestSubmission') import Pokestop = require('server/src/models/Pokestop') -import { ModelReturn } from './utility' +import { ModelReturn, OnlyType } from './utility' import { Profile } from 'passport-discord' import { User } from './models' @@ -235,3 +235,20 @@ export type DiscordVerifyFunction = ( profile: Profile, done: VerifyCallback, ) => void + +export type BaseFilter = import('server/src/services/filters/Base') + +export type PokemonFilter = + import('server/src/services/filters/pokemon/Frontend') + +export type AllFilters = ReturnType< + typeof import('server/src/services/filters/builder/base') +> + +export type Categories = keyof AllFilters + +export type AdvCategories = 'pokemon' | 'gyms' | 'pokestops' | 'nests' + +export type UIObject = ReturnType< + typeof import('server/src/services/ui/primary') +> diff --git a/packages/types/package.json b/packages/types/package.json index ba77822e0..3b9f5c51d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -6,13 +6,15 @@ "scripts": { "sort": "npx sort-package-json", "depCheck": "npx depcheck", - "eslint:check": "eslint \"**/*.{js,jsx}\"", - "eslint:fix": "eslint \"**/*.{js,jsx}\" --fix", - "prettier:check": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", + "lint": "eslint \"**/*.{js,jsx}\"", + "lint:fix": "eslint \"**/*.{js,jsx}\" --fix", + "prettier": "prettier --check \"**/*.{css,html,js,jsx,yml}\"", "prettier:fix": "prettier --write \"**/*.{css,html,js,jsx,yml}\"" }, "devDependencies": { "@apollo/client": "^3.7.15", + "@mui/material": "^5.14.0", + "@mui/system": "^5.15.2", "@sentry/node": "^7.48.0", "@types/config": "^3.3.0", "@types/node": "^20.5.1", @@ -26,4 +28,4 @@ "passport-discord": "https://github.com/tonestrike/passport-discord.git", "passport-oauth2": "^1.7.0" } -} \ No newline at end of file +} diff --git a/server/src/configs/custom-environment-variables.json b/server/src/configs/custom-environment-variables.json index a65e52ab8..242f68e9c 100644 --- a/server/src/configs/custom-environment-variables.json +++ b/server/src/configs/custom-environment-variables.json @@ -1060,6 +1060,14 @@ "__name": "DEFAULT_FILTERS_POKEMON_ENABLED", "__format": "boolean" }, + "easyMode": { + "__name": "DEFAULT_FILTERS_POKEMON_EASY_MODE", + "__format": "boolean" + }, + "onlyShowAvailable": { + "__name": "DEFAULT_FILTERS_POKEMON_ONLY_SHOW_AVAILABLE", + "__format": "boolean" + }, "legacyFilter": { "__name": "DEFAULT_FILTERS_POKEMON_LEGACY_FILTER", "__format": "boolean" @@ -1919,6 +1927,24 @@ "__name": "ICONS_SIZES_SPAWNPOINT_XL", "__format": "number" } + }, + "event": { + "sm": { + "__name": "ICONS_SIZES_EVENT_SM", + "__format": "number" + }, + "md": { + "__name": "ICONS_SIZES_EVENT_MD", + "__format": "number" + }, + "lg": { + "__name": "ICONS_SIZES_EVENT_LG", + "__format": "number" + }, + "xl": { + "__name": "ICONS_SIZES_EVENT_XL", + "__format": "number" + } } } }, diff --git a/server/src/configs/default.json b/server/src/configs/default.json index cda317d85..609889454 100644 --- a/server/src/configs/default.json +++ b/server/src/configs/default.json @@ -508,6 +508,8 @@ }, "pokemon": { "enabled": false, + "easyMode": true, + "onlyShowAvailable": true, "legacyFilter": false, "allPokemon": false, "globalValues": { @@ -953,6 +955,12 @@ "md": 1, "lg": 2, "xl": 3 + }, + "event": { + "sm": 20, + "md": 40, + "lg": 60, + "xl": 80 } } }, diff --git a/server/src/graphql/resolvers.js b/server/src/graphql/resolvers.js index 863d9f3e6..394fb9595 100644 --- a/server/src/graphql/resolvers.js +++ b/server/src/graphql/resolvers.js @@ -22,9 +22,16 @@ const resolvers = { available: (_, _args, { Event, Db, perms }) => { const data = { pokemon: perms.pokemon ? Event.available.pokemon : [], - gyms: perms.gyms ? Event.available.gyms : [], + gyms: perms.gyms || perms.raids ? Event.available.gyms : [], nests: perms.nests ? Event.available.nests : [], - pokestops: perms.pokestops ? Event.available.pokestops : [], + pokestops: + perms.pokestops || + perms.invasions || + perms.eventStops || + perms.quests || + perms.lures + ? Event.available.pokestops + : [], questConditions: perms.quests ? Db.questConditions : {}, masterfile: { ...Event.masterfile, invasions: Event.invasions }, filters: buildDefaultFilters(perms, Db), diff --git a/server/src/graphql/server.js b/server/src/graphql/server.js index 7802d98ec..9b850c21f 100644 --- a/server/src/graphql/server.js +++ b/server/src/graphql/server.js @@ -81,6 +81,7 @@ async function startApollo(httpServer) { async requestDidStart(requestContext) { requestContext.contextValue.startTime = Date.now() + log.debug(requestContext.request?.variables?.filters) return { async willSendResponse(context) { const filterCount = Object.keys( @@ -98,7 +99,6 @@ async function startApollo(httpServer) { const data = response.body.singleResult.data?.[endpoint] const returned = Array.isArray(data) ? data.length : 0 - log.info( HELPERS[endpoint] || `[${endpoint?.toUpperCase()}]`, '|', diff --git a/server/src/models/Gym.js b/server/src/models/Gym.js index 6fda87ddc..41dd9c9e0 100644 --- a/server/src/models/Gym.js +++ b/server/src/models/Gym.js @@ -187,13 +187,16 @@ class Gym extends Model { ) teams.forEach((team) => { - let slotCount = 0 - slots.forEach((slot) => { - if (slot.team === team) { - slotCount += 1 - finalSlots[team].push(+slot.slots) - } - }) + const all = args.filters[`t${team}-0`]?.all + let slotCount = all ? baseGymSlotAmounts.length : 0 + if (!all) { + slots.forEach((slot) => { + if (slot.team === team) { + slotCount += 1 + finalSlots[team].push(+slot.slots) + } + }) + } if (slotCount === baseGymSlotAmounts.length || team == 0) { delete finalSlots[team] finalTeams.push(+team) @@ -436,12 +439,9 @@ class Gym extends Model { .then((r) => { const unique = new Set() r.forEach((result) => { - if (result.team) { - if (result.slots) { - unique.add(`g${result.team}-${result.slots}`) - } else { - unique.add(`t${result.team}-0`) - } + if (result.team !== null && result.slots !== null) { + unique.add(`t${result.team}-0`) + unique.add(`g${result.team}-${6 - result.slots}`) } }) return [...unique] diff --git a/server/src/models/Pokemon.js b/server/src/models/Pokemon.js index e78812ecb..bd86925af 100644 --- a/server/src/models/Pokemon.js +++ b/server/src/models/Pokemon.js @@ -102,6 +102,10 @@ class Pokemon extends Model { } }) + if (Object.keys(filterMap).length === 0 && mods.onlyEasyMode) { + // if no pokemon are present we want global filters to apply still + mods.onlyLinkGlobal = false + } const globalFilter = new PkmnFilter( 'global', args.filters.onlyIvOr, @@ -243,10 +247,23 @@ class Pokemon extends Model { const [id, form] = key.split('-', 2).map(Number) return { id, form } }) - if (!globalFilter.mods.onlyLinkGlobal) pokemon.push({ id: -1 }) // add everything else - filters.push(...globalFilter.buildApiFilter(pokemon)) - if (onlyZeroIv) filters.push({ iv: { min: 0, max: 0 }, pokemon }) - if (onlyHundoIv) filters.push({ iv: { min: 100, max: 100 }, pokemon }) + if (!globalFilter.mods.onlyLinkGlobal) { + pokemon.push({ id: -1 }) // add everything else + } + if ( + globalFilter.mods.onlyLinkGlobal + ? !!filters.length || globalFilter.mods.onlyEasyMode + : true + ) { + filters.push(...globalFilter.buildApiFilter(pokemon)) + } + const globalPokes = globalFilter.mods.onlyLinkGlobal + ? [...pokemon, { id: -1 }] + : pokemon + if (onlyZeroIv) + filters.push({ iv: { min: 0, max: 0 }, pokemon: globalPokes }) + if (onlyHundoIv) + filters.push({ iv: { min: 100, max: 100 }, pokemon: globalPokes }) } /** @type {import("../types").Pokemon[]} */ const results = await this.evalQuery( diff --git a/server/src/models/Pokestop.js b/server/src/models/Pokestop.js index f2598fcda..d4240435e 100644 --- a/server/src/models/Pokestop.js +++ b/server/src/models/Pokestop.js @@ -871,7 +871,7 @@ class Pokestop extends Model { quest.quest_timestamp >= midnight && (filters.onlyAllPokestops || (filters[newQuest.key] && - (filters[newQuest.key].adv + (filters[newQuest.key].adv && !filters[newQuest.key].all ? filters[newQuest.key].adv.includes(quest.quest_title) : true)) || filters[`u${quest.quest_reward_type}`]) @@ -1334,6 +1334,7 @@ class Pokestop extends Model { if (hasConfirmed) { queries.rocketPokemon = this.query() .select([ + 'character AS grunt_type', 'slot_1_pokemon_id', 'slot_1_form', 'slot_2_pokemon_id', @@ -1345,6 +1346,7 @@ class Pokestop extends Model { .whereNotNull('slot_2_pokemon_id') .whereNotNull('slot_3_pokemon_id') .groupBy([ + 'character', 'slot_1_pokemon_id', 'slot_1_form', 'slot_2_pokemon_id', @@ -1482,15 +1484,22 @@ class Pokestop extends Model { case 'rocketPokemon': if (hasConfirmed) { rewards.forEach((reward) => { - finalList.add( - `a${reward.slot_1_pokemon_id}-${reward.slot_1_form}`, - ) - finalList.add( - `a${reward.slot_2_pokemon_id}-${reward.slot_2_form}`, - ) - finalList.add( - `a${reward.slot_3_pokemon_id}-${reward.slot_3_form}`, - ) + const fullGrunt = Event.invasions[reward.grunt_type] + if (fullGrunt?.firstReward) { + finalList.add( + `a${reward.slot_1_pokemon_id}-${reward.slot_1_form}`, + ) + } + if (fullGrunt?.secondReward) { + finalList.add( + `a${reward.slot_2_pokemon_id}-${reward.slot_2_form}`, + ) + } + if (fullGrunt?.thirdReward) { + finalList.add( + `a${reward.slot_3_pokemon_id}-${reward.slot_3_form}`, + ) + } }) } break diff --git a/server/src/services/EventManager.js b/server/src/services/EventManager.js index 509ae5fbe..75a65453e 100644 --- a/server/src/services/EventManager.js +++ b/server/src/services/EventManager.js @@ -48,6 +48,42 @@ class EventManager { */ async setAvailable(category, model, Db) { this.available[category] = await Db.getAvailable(model) + + /** @param {string} key */ + const parseKey = (key) => { + const match = key.match(/([a-zA-Z]*)(\d+)(?:-(\d+))?/) + return { + letter: match[1], + firstNumber: parseInt(match[2], 10), + secondNumber: match[3] ? parseInt(match[3], 10) : null, + } + } + + this.available[category].sort((a, b) => { + const keyA = parseKey(a) + const keyB = parseKey(b) + + // Compare by letter + if (keyA.letter !== keyB.letter) { + if (keyA.letter === '') return 1 // No letter comes last + if (keyB.letter === '') return -1 + return keyA.letter.localeCompare(keyB.letter) + } + + // Compare by the first number + if (keyA.firstNumber !== keyB.firstNumber) { + return keyA.firstNumber - keyB.firstNumber + } + + // Compare by the second number (if exists) + if (keyA.secondNumber !== null || keyB.secondNumber !== null) { + if (keyA.secondNumber === null) return -1 + if (keyB.secondNumber === null) return 1 + return keyA.secondNumber - keyB.secondNumber + } + + return 0 + }) } /** diff --git a/server/src/services/api/Poracle.js b/server/src/services/api/Poracle.js index 27e0eb5a9..dc7d3019d 100644 --- a/server/src/services/api/Poracle.js +++ b/server/src/services/api/Poracle.js @@ -784,7 +784,7 @@ class PoracleAPI { gym_id: null, byDistance: false, allMoves: true, - allForms: true, + allForms: false, everything_individually: this.everythingFlagPermissions === 'allow-and-always-individually' || @@ -1088,7 +1088,7 @@ class PoracleAPI { amount: 0, form: 0, byDistance: false, - allForms: true, + allForms: false, }, ui: { general: { @@ -1152,7 +1152,7 @@ class PoracleAPI { min_spawn_avg: 0, form: 0, byDistance: false, - allForms: true, + allForms: false, }, ui: { general: { diff --git a/server/src/services/filters/Base.js b/server/src/services/filters/Base.js index dfbfd6382..4289cebdd 100644 --- a/server/src/services/filters/Base.js +++ b/server/src/services/filters/Base.js @@ -4,10 +4,12 @@ class BaseFilter { * * @param {boolean} [enabled] * @param {'sm' | 'md' | 'lg' | 'xl'} [size] + * @param {boolean} [all] */ - constructor(enabled, size) { + constructor(enabled, size, all) { this.enabled = enabled || false this.size = size || 'md' + this.all = all || false this.adv = '' } } diff --git a/server/src/services/filters/builder/base.js b/server/src/services/filters/builder/base.js index 985d4c514..6b523afd0 100644 --- a/server/src/services/filters/builder/base.js +++ b/server/src/services/filters/builder/base.js @@ -54,6 +54,7 @@ function buildDefaultFilters(perms, database) { : undefined, badge: perms.gymBadges ? 'all' : undefined, raidTier: perms.raids ? 'all' : undefined, + standard: new BaseFilter(), filter: { ...buildGyms(perms, defaultFilters.gyms), ...pokemon.raids, @@ -67,6 +68,7 @@ function buildDefaultFilters(perms, database) { pokemon: defaultFilters.nests.pokemon, polygons: defaultFilters.nests.polygons, avgFilter: defaultFilters.nests.avgFilter, + standard: new BaseFilter(), filter: pokemon.nests, } : undefined, @@ -93,6 +95,7 @@ function buildDefaultFilters(perms, database) { ? defaultFilters.pokestops.invasions : undefined, arEligible: perms.pokestops ? false : undefined, + standard: new BaseFilter(), filter: { ...pokemon.rocket, ...buildPokestops(perms, defaultFilters.pokestops), @@ -104,6 +107,8 @@ function buildDefaultFilters(perms, database) { perms.pokemon && database.models.Pokemon ? { enabled: defaultFilters.pokemon.enabled, + easyMode: defaultFilters.pokemon.easyMode, + onlyShowAvailable: defaultFilters.pokemon.onlyShowAvailable, legacy: pokemonReducer && config.getSafe('map.misc.enableMapJsFilter') ? defaultFilters.pokemon.legacyFilter @@ -128,6 +133,7 @@ function buildDefaultFilters(perms, database) { 0, Math.ceil(database.filterContext.Route.maxDistance / 1000) + 1, ], + standard: new BaseFilter(), filter: { global: new BaseFilter(), }, @@ -137,6 +143,7 @@ function buildDefaultFilters(perms, database) { perms.portals && database.models.Portal ? { enabled: defaultFilters.portals.enabled, + standard: new BaseFilter(), filter: { global: new BaseFilter(), old: new BaseFilter(), @@ -147,6 +154,7 @@ function buildDefaultFilters(perms, database) { scanAreas: perms.scanAreas ? { enabled: defaultFilters.scanAreas.enabled, + standard: new BaseFilter(), filterByAreas: false, filter: { areas: [], search: '' }, } @@ -159,6 +167,7 @@ function buildDefaultFilters(perms, database) { s17Cells: defaultFilters.submissionCells.s17Cells, s14Cells: defaultFilters.submissionCells.s14Cells, includeSponsored: defaultFilters.submissionCells.includeSponsored, + standard: new BaseFilter(), filter: { global: new BaseFilter() }, } : undefined, @@ -166,6 +175,7 @@ function buildDefaultFilters(perms, database) { ? { enabled: defaultFilters.s2cells.enabled, cells: defaultFilters.s2cells.cells, + standard: new BaseFilter(), filter: { global: new BaseFilter() }, } : undefined, @@ -173,6 +183,7 @@ function buildDefaultFilters(perms, database) { perms.weather && database.models.Weather ? { enabled: defaultFilters.weather.enabled, + standard: new BaseFilter(), filter: { global: new BaseFilter() }, } : undefined, @@ -180,6 +191,7 @@ function buildDefaultFilters(perms, database) { perms.spawnpoints && database.models.Spawnpoint ? { enabled: defaultFilters.spawnpoints.enabled, + standard: new BaseFilter(), tth: defaultFilters.spawnpoints.tth, filter: { global: new BaseFilter(), @@ -192,6 +204,7 @@ function buildDefaultFilters(perms, database) { perms.scanCells && database.models.ScanCell ? { enabled: defaultFilters.scanCells.enabled, + standard: new BaseFilter(), filter: { global: new BaseFilter() }, } : undefined, @@ -199,6 +212,7 @@ function buildDefaultFilters(perms, database) { perms.devices && database.models.Device ? { enabled: defaultFilters.devices.enabled, + standard: new BaseFilter(), filter: { online: new BaseFilter(), offline: new BaseFilter(), diff --git a/server/src/services/filters/builder/gym.js b/server/src/services/filters/builder/gym.js index 3b600c02d..4528be7d1 100644 --- a/server/src/services/filters/builder/gym.js +++ b/server/src/services/filters/builder/gym.js @@ -6,10 +6,9 @@ const BaseFilter = require('../Base') * * @param {import("@rm/types").Permissions} perms * @param {import("@rm/types").Config['defaultFilters']['gyms']} defaults - * @returns */ function buildGyms(perms, defaults) { - const gymFilters = {} + const gymFilters = /** @type {Record} */ ({}) if (perms.gyms) { Object.keys(Event.masterfile.teams).forEach((team, i) => { diff --git a/server/src/services/filters/builder/pokemon.js b/server/src/services/filters/builder/pokemon.js index c6d45c95d..38dd7dc42 100644 --- a/server/src/services/filters/builder/pokemon.js +++ b/server/src/services/filters/builder/pokemon.js @@ -9,8 +9,14 @@ const BaseFilter = require('../Base') * * @param {import("@rm/types").Config['defaultFilters']} defaults * @param {import('../pokemon/Frontend')} base - * @param {*} custom - * @returns + * @param {import('@rm/types').PokemonFilter} custom + * @returns {{ + * full: { [key: string]: import('@rm/types').PokemonFilter }, + * raids: { [key: string]: BaseFilter }, + * quests: { [key: string]: BaseFilter }, + * nests: { [key: string]: BaseFilter }, + * rocket: { [key: string]: BaseFilter }, + * }} */ function buildPokemon(defaults, base, custom) { const pokemon = { @@ -39,15 +45,17 @@ function buildPokemon(defaults, base, custom) { } pokemon.nests[`${i}-${j}`] = new BaseFilter(defaults.nests.allPokemon) } - if (pkmn.family == i) { - pokemon.quests[`c${pkmn.family}`] = new BaseFilter( - defaults.pokestops.candy, - ) - pokemon.quests[`x${pkmn.family}`] = new BaseFilter( - defaults.pokestops.candy, - ) + if ('family' in pkmn) { + if (pkmn.family === +i) { + pokemon.quests[`c${pkmn.family}`] = new BaseFilter( + defaults.pokestops.candy, + ) + pokemon.quests[`x${pkmn.family}`] = new BaseFilter( + defaults.pokestops.candy, + ) + } } - if (pkmn.tempEvolutions) { + if ('tempEvolutions' in pkmn) { energyAmounts.forEach((a) => { pokemon.quests[`m${i}-${a}`] = new BaseFilter( defaults.pokestops.megaEnergy, diff --git a/server/src/services/filters/builder/pokestop.js b/server/src/services/filters/builder/pokestop.js index 0ceb80587..97dcb2eb1 100644 --- a/server/src/services/filters/builder/pokestop.js +++ b/server/src/services/filters/builder/pokestop.js @@ -8,7 +8,7 @@ const { Event } = require('../../initialization') * * @param {import("@rm/types").Permissions} perms * @param {import("@rm/types").Config['defaultFilters']['pokestops']} defaults - * @returns + * @returns {Record} */ function buildPokestops(perms, defaults) { const quests = { s0: new BaseFilter() } diff --git a/server/src/services/filters/pokemon/Backend.js b/server/src/services/filters/pokemon/Backend.js index 1d7a38d02..b204f6ec2 100644 --- a/server/src/services/filters/pokemon/Backend.js +++ b/server/src/services/filters/pokemon/Backend.js @@ -40,11 +40,12 @@ module.exports = class PkmnBackend { * @param {boolean} mods.onlyHundoIv * @param {boolean} mods.onlyZeroIv * @param {boolean} mods.onlyAllPvp + * @param {boolean} mods.onlyEasyMode * @param {string[]} mods.onlyAreas * @param {boolean} mods.onlyLegacy */ constructor(id, filter, global, perms, mods) { - const [pokemon, form] = id.split('-').map(Number) + const [pokemon, form] = id.split('-', 2).map(Number) this.id = id this.pokemon = pokemon || 0 this.form = form || 0 @@ -58,6 +59,8 @@ module.exports = class PkmnBackend { this.globalKeys = this.getRelevantKeys(global) this.expertFilter = this.getCallback(id === 'global') this.expertGlobal = this.getCallback(true) + this.isEqualToGlobal = + this.expertFilter.toString() === this.expertGlobal.toString() } get keyArray() { @@ -141,7 +144,7 @@ module.exports = class PkmnBackend { if (merged) merged = `(${merged})&` merged += `G${filter.gender}` } - log.debug(HELPERS.pokemon, this.id, { + log.trace(HELPERS.pokemon, this.id, { andStr, orStr, merged, @@ -171,13 +174,15 @@ module.exports = class PkmnBackend { * @returns {Set<(typeof import("./constants").KEYS)[number]>} */ getRelevantKeys(filter = this.filter) { - return new Set( - KEYS.filter( - (key) => - (pvpConfig.leagueObj[key] ? this.perms.pvp : this.perms.iv) && - this.isActive(key, filter), - ), - ) + return this.filter.all + ? new Set() + : new Set( + KEYS.filter( + (key) => + (pvpConfig.leagueObj[key] ? this.perms.pvp : this.perms.iv) && + this.isActive(key, filter), + ), + ) } /** @@ -278,18 +283,21 @@ module.exports = class PkmnBackend { xxl, ...rest } = this.filter - if (pokemon === undefined && this.id !== 'global') - pokemon = [{ id: this.pokemon, form: this.form }] + if (this.id !== 'global') { + if (pokemon === undefined) { + pokemon = [{ id: this.pokemon, form: this.form }] + } + if (!this.filterKeys.size || (!this.perms.iv && !this.perms.pvp)) { + return [{ pokemon, iv: { min: -1, max: 100 } }] + } + if (this.isEqualToGlobal) { + return [] + } + } if (this.mods.onlyLegacy) { return dnfifyIvFilter(adv, pokemon) } - if ( - this.id !== 'global' && - (!this.filterKeys.size || (!this.perms.iv && !this.perms.pvp)) - ) { - return [{ pokemon, iv: { min: -1, max: 100 } }] - } - const results = /** @type {import('../../../types').DnfFilter[]} */ ([]) + const results = /** @type {import('@rm/types').DnfFilter[]} */ ([]) if ( ['iv', 'atk_iv', 'def_iv', 'sta_iv', 'cp', 'level', 'gender'].some((k) => this.filterKeys.has(k), @@ -350,6 +358,8 @@ module.exports = class PkmnBackend { ) { if ( !this.mods.onlyLinkGlobal || + (this.mods.onlyHundoIv && pokemon.iv === 100) || + (this.mods.onlyZeroIv && pokemon.iv === 0) || (this.pokemon === pokemon.pokemon_id && this.form === pokemon.form) ) { if (!this.expertFilter || !this.expertGlobal) return true @@ -367,13 +377,14 @@ module.exports = class PkmnBackend { /** * @param {import("@rm/types").Pokemon} pokemon * @param {number} [ts] - * @returns {{ cleanPvp: { [key in typeof LEAGUES[number]]?: number[] }, bestPvp: number }} + * @returns {{ cleanPvp: { [key in typeof LEAGUES[number]]?: import('@rm/types').PvpEntry[] }, bestPvp: number }} */ buildPvp(pokemon, ts = Math.floor(Date.now() / 1000)) { const parsed = pvpConfig.reactMapHandlesPvp ? Pvp.resultWithCache(pokemon, ts) : getParsedPvp(pokemon) - const cleanPvp = {} + const cleanPvp = + /** @type {{ [key in typeof LEAGUES[number]]?: import('@rm/types').PvpEntry[] }} */ ({}) let bestPvp = 4096 Object.keys(parsed).forEach((league) => { if (pvpConfig.leagueObj[league]) { @@ -414,7 +425,7 @@ module.exports = class PkmnBackend { if (result.pokemon_id === 132 && !result.ditto_form) { result.ditto_form = result.form result.form = - Event.masterfile.pokemon[result.pokemon_id]?.defaultFormId || 0 + Event.masterfile.pokemon[result.display_pokemon_id]?.defaultFormId || 0 } if (!result.seen_type) { if (result.spawn_id === null) { @@ -442,7 +453,7 @@ module.exports = class PkmnBackend { 'weather', ]) } - if (this.perms.pvp && result.cp) { + if (this.perms.pvp && pokemon.cp) { const { cleanPvp, bestPvp } = this.buildPvp(pokemon) result.bestPvp = bestPvp result.cleanPvp = cleanPvp diff --git a/server/src/services/filters/pokemon/Frontend.js b/server/src/services/filters/pokemon/Frontend.js index f53d4dc71..16c5007a4 100644 --- a/server/src/services/filters/pokemon/Frontend.js +++ b/server/src/services/filters/pokemon/Frontend.js @@ -2,7 +2,7 @@ const config = require('@rm/config') const BaseFilter = require('../Base') -module.exports = class PokemonFilter extends BaseFilter { +class PokemonFilter extends BaseFilter { /** * @param {boolean} [enabled] * @param {'sm' | 'md' | 'lg' | 'xl'} [size] @@ -52,3 +52,5 @@ module.exports = class PokemonFilter extends BaseFilter { ) } } + +module.exports = PokemonFilter diff --git a/server/src/services/ui/primary.js b/server/src/services/ui/primary.js index a62315dce..cbc0b6036 100644 --- a/server/src/services/ui/primary.js +++ b/server/src/services/ui/primary.js @@ -5,75 +5,78 @@ const { Db } = require('../initialization') const nestFilters = config.getSafe('defaultFilters.nests') const leagues = config.getSafe('api.pvp.leagues') -const SLIDERS = { - pokemon: { - primary: [ - { - name: 'iv', - label: '%', - min: 0, - max: 100, - perm: 'iv', - color: 'secondary', - }, - ], - secondary: [ - { - name: 'level', - label: '', - min: 1, - max: 35, - perm: 'iv', - color: 'secondary', - }, - { - name: 'atk_iv', - label: '', - min: 0, - max: 15, - perm: 'iv', - color: 'secondary', - }, - { - name: 'def_iv', - label: '', - min: 0, - max: 15, - perm: 'iv', - color: 'secondary', - }, - { - name: 'sta_iv', - label: '', - min: 0, - max: 15, - perm: 'iv', - color: 'secondary', - }, - { - name: 'cp', - label: '', - min: 10, - max: 5000, - perm: 'iv', - color: 'secondary', - }, - ], - }, - nests: { - secondary: [ - { - name: 'avgFilter', - i18nKey: 'spawns_per_hour', - label: '', - min: nestFilters.avgFilter[0], - max: nestFilters.avgFilter[1], - perm: 'nests', - step: nestFilters.avgSliderStep, - }, - ], - }, -} +/** @typedef {import('@rm/types').RMSlider} Slider */ + +const SLIDERS = + /** @type {{ pokemon: { primary: Slider[], secondary: Slider[] }, nests: { secondary: Slider[] } }} */ ({ + pokemon: { + primary: [ + { + name: 'iv', + label: '%', + min: 0, + max: 100, + perm: 'iv', + color: 'secondary', + }, + ], + secondary: [ + { + name: 'level', + label: '', + min: 1, + max: 35, + perm: 'iv', + color: 'secondary', + }, + { + name: 'atk_iv', + label: '', + min: 0, + max: 15, + perm: 'iv', + color: 'secondary', + }, + { + name: 'def_iv', + label: '', + min: 0, + max: 15, + perm: 'iv', + color: 'secondary', + }, + { + name: 'sta_iv', + label: '', + min: 0, + max: 15, + perm: 'iv', + color: 'secondary', + }, + { + name: 'cp', + label: '', + min: 10, + max: 5000, + perm: 'iv', + color: 'secondary', + }, + ], + }, + nests: { + secondary: [ + { + name: 'avgFilter', + i18nKey: 'spawns_per_hour', + label: '', + min: nestFilters.avgFilter[0], + max: nestFilters.avgFilter[1], + perm: 'nests', + step: nestFilters.avgSliderStep, + }, + ], + }, + }) leagues.forEach((league) => SLIDERS.pokemon.primary.push({ @@ -86,6 +89,9 @@ leagues.forEach((league) => }), ) +// TODO this will be used later in the config +const BLOCKED = undefined + /** * * @param {import('express').Request} req @@ -98,41 +104,45 @@ function generateUi(req, perms) { gyms: (perms.gyms || perms.raids) && Db.models.Gym ? { - allGyms: true, - raids: perms.raids, - exEligible: true, - inBattle: true, - arEligible: true, - gymBadges: perms.gymBadges, + allGyms: perms.gyms || BLOCKED, + raids: perms.raids || BLOCKED, + exEligible: perms.gyms || BLOCKED, + inBattle: perms.gyms || BLOCKED, + arEligible: perms.gyms || BLOCKED, + gymBadges: perms.gymBadges || BLOCKED, } - : undefined, + : BLOCKED, nests: perms.nests && Db.models.Nest - ? { pokemon: true, polygons: true, sliders: SLIDERS.nests } - : undefined, + ? { + pokemon: true, + polygons: true, + sliders: SLIDERS.nests, + } + : BLOCKED, pokestops: (perms.pokestops || perms.lures || perms.quests || perms.invasions) && Db.models.Pokestop ? { - allPokestops: perms.pokestops, - lures: perms.lures, - eventStops: perms.eventStops, - quests: perms.quests, - invasions: perms.invasions, - arEligible: perms.pokestops, + allPokestops: perms.pokestops || BLOCKED, + lures: perms.lures || BLOCKED, + eventStops: perms.eventStops || BLOCKED, + quests: perms.quests || BLOCKED, + invasions: perms.invasions || BLOCKED, + arEligible: perms.pokestops || BLOCKED, } - : undefined, + : BLOCKED, pokemon: (perms.pokemon || perms.iv || perms.pvp) && Db.models.Pokemon ? { - legacy: mapConfig.misc.enableMapJsFilter, - iv: perms.iv, - pvp: perms.pvp, - standard: true, - ivOr: true, - gender: true, - zeroIv: perms.iv, - hundoIv: perms.iv, + legacy: perms.iv && mapConfig.misc.enableMapJsFilter, + iv: perms.iv || BLOCKED, + pvp: perms.pvp || BLOCKED, + // standard: true, + // ivOr: true, + // gender: true, + zeroIv: perms.iv || BLOCKED, + hundoIv: perms.iv || BLOCKED, sliders: { primary: SLIDERS.pokemon.primary.map((slider) => ({ ...slider, @@ -144,34 +154,34 @@ function generateUi(req, perms) { })), }, } - : undefined, - routes: perms.routes && Db.models.Route ? { enabled: true } : undefined, + : BLOCKED, + routes: perms.routes && Db.models.Route ? { enabled: true } : BLOCKED, wayfarer: perms.portals || perms.submissionCells ? { - portals: !!(perms.portals && Db.models.Portal) || undefined, + portals: !!(perms.portals && Db.models.Portal) || BLOCKED, submissionCells: !!( perms.submissionCells && Db.models.Pokestop && Db.models.Gym - ) || undefined, + ) || BLOCKED, } : undefined, - s2cells: perms.s2cells ? { enabled: true, cells: true } : undefined, + s2cells: perms.s2cells ? { enabled: true } : BLOCKED, scanAreas: perms.scanAreas ? { filterByAreas: true, enabled: true } : undefined, - weather: perms.weather && Db.models.Weather ? { enabled: true } : undefined, + weather: perms.weather && Db.models.Weather ? { enabled: true } : BLOCKED, admin: perms.spawnpoints || perms.scanCells || perms.devices ? { spawnpoints: - !!(perms.spawnpoints && Db.models.Spawnpoint) || undefined, - scanCells: !!(perms.scanCells && Db.models.ScanCell) || undefined, - devices: !!(perms.devices && Db.models.Device) || undefined, + !!(perms.spawnpoints && Db.models.Spawnpoint) || BLOCKED, + scanCells: !!(perms.scanCells && Db.models.ScanCell) || BLOCKED, + devices: !!(perms.devices && Db.models.Device) || BLOCKED, } - : undefined, + : BLOCKED, settings: true, } diff --git a/server/src/strategies/local.js b/server/src/strategies/local.js index d1f00579e..d1c1abf4a 100644 --- a/server/src/strategies/local.js +++ b/server/src/strategies/local.js @@ -104,6 +104,7 @@ const authHandler = async (_req, username, password, done) => { user.discordId = userExists.discordId user.telegramId = userExists.telegramId user.webhookStrategy = userExists.webhookStrategy + user.data = userExists.data user.status = userExists.data ? (typeof userExists.data === 'string' ? JSON.parse(userExists.data).status diff --git a/src/assets/constants.js b/src/assets/constants.js new file mode 100644 index 000000000..727401b55 --- /dev/null +++ b/src/assets/constants.js @@ -0,0 +1,53 @@ +export const ICON_SIZES = /** @type {const} */ (['sm', 'md', 'lg', 'xl']) + +export const XXS_XXL = /** @type {const} */ (['xxs', 'xxl']) + +export const NUNDO_HUNDO = /** @type {const} */ (['zeroIv', 'hundoIv']) + +export const ENUM_GENDER = /** @type {const} */ ([0, 1, 2, 3]) + +export const ENUM_BADGES = /** @type {const} */ ([0, 1, 2, 3]) + +export const S2_LEVELS = /** @type {const} */ ([ + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, +]) + +export const FORT_LEVELS = /** @type {const} */ (['all', '1', '2', '3']) + +export const BADGES = /** @type {const} */ ([ + 'all', + 'badge_1', + 'badge_2', + 'badge_3', +]) + +export const QUEST_SETS = /** @type {const} */ ([ + 'with_ar', + 'both', + 'without_ar', +]) + +export const WAYFARER_OPTIONS = /** @type {const} */ ([ + 'rings', + 'includeSponsored', + 's14Cells', + 's17Cells', +]) + +export const ENUM_TTH = /** @type {const} */ ([0, 1, 2]) + +export const MIN_MAX = /** @type {const} */ (['min', 'max']) + +export const ENABLED_ALL = /** @type {const} */ (['enabled', 'all']) + +export const RADIUS_CHOICES = /** @type {const} */ (['pokemon', 'gym']) + +export const METHODS = /** @type {const} */ (['discord', 'telegram']) + +export const FILTER_SKIP_LIST = ['filter', 'enabled', 'legacy'] + +export const ALWAYS_EXCLUDED = new Set(['donor', 'blockedGuildNames', 'admin']) + +export const SCAN_MODES = /** @type */ (['confirmed', 'loading', 'error']) + +export const SCAN_SIZES = /** @type {const} */ (['S', 'M', 'XL']) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 6a720ae6b..1a5ca1502 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -55,6 +55,11 @@ body { border-radius: 0.5rem !important; } +.leaflet-control-locate > a { + font-size: 16.8px; + padding-left: 1.5px; +} + .table-pvp { margin-left: auto; margin-right: auto; @@ -387,15 +392,6 @@ img { font-size: 0; } -.disabled-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - opacity: 80%; -} - .perm-wrapper img { width: 100%; } @@ -481,3 +477,73 @@ input[type='time']::-webkit-calendar-picker-indicator { .marker-cluster span { line-height: 30px; } + +.container { + display: grid; + grid-template-columns: 25% 75%; +} + +@media screen and (max-width: 600px) { + .container { + display: grid; + grid-template-columns: 100%; + grid-template-rows: 100%; + } +} + +.column-25 { + max-height: 75vh; + overflow: auto; +} + +.column-75 { + display: grid; + grid-template-rows: max-content 1fr; +} + +.vgrid-item { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: auto auto 10%; +} + +.vgrid-icon { + grid-column: 3; + grid-row: 1; + justify-self: end; + align-self: start; +} + +.vgrid-image { + grid-column: 1 / 4; + grid-row: 1 / 3; + justify-self: center; + align-self: center; + display: relative; + padding: 0; +} + +.vgrid-caption { + grid-column: 1 / 4; + grid-row: 3; + text-align: center; + justify-self: center; + align-self: center; + width: 100%; +} + +.badge-diamond { + clip-path: polygon(50% 0%, 85% 55%, 50% 100%, 15% 55%); + position: absolute; +} + +.disabled-overlay { + position: absolute; + color: black; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 90%; + z-index: 9; +} diff --git a/src/assets/mui/global.jsx b/src/assets/mui/global.jsx index 4f14f97c4..327cb5017 100644 --- a/src/assets/mui/global.jsx +++ b/src/assets/mui/global.jsx @@ -1,10 +1,10 @@ import * as React from 'react' import GlobalStyles from '@mui/material/GlobalStyles' import { darken, lighten } from '@mui/material/styles' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' export function ApplyGlobal() { - const online = useStatic((s) => s.online) + const online = useMemory((s) => s.online) return ( s.theme.primary) - const secondary = useStatic((s) => s.theme.secondary) - const darkMode = useStore((s) => s.darkMode) + const primary = useMemory((s) => s.theme.primary) + const secondary = useMemory((s) => s.theme.secondary) + const darkMode = useStorage((s) => s.darkMode) if (darkMode) { if (!document.body.classList.contains('dark')) { diff --git a/src/components/ClearStorage.jsx b/src/components/ClearStorage.jsx index 29e08e9e2..1a9b744ec 100644 --- a/src/components/ClearStorage.jsx +++ b/src/components/ClearStorage.jsx @@ -1,14 +1,16 @@ // @ts-check -import { useStatic, useStore } from '@hooks/useStore' import * as React from 'react' import { Navigate } from 'react-router-dom' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' + export default function ClearStorage() { localStorage.clear() sessionStorage.clear() React.useEffect(() => { - useStore.setState({ + useStorage.setState({ filters: {}, menus: {}, location: [ @@ -17,7 +19,7 @@ export default function ClearStorage() { ], zoom: CONFIG.map.general.startZoom, }) - useStatic.setState({ Icons: null }) + useMemory.setState({ Icons: null }) }, []) return } diff --git a/src/components/Clustering.jsx b/src/components/Clustering.jsx index 050ce86a4..3d8d85390 100644 --- a/src/components/Clustering.jsx +++ b/src/components/Clustering.jsx @@ -3,7 +3,8 @@ import * as React from 'react' import { useMap, GeoJSON } from 'react-leaflet' import Supercluster from 'supercluster' import { marker, divIcon, point } from 'leaflet' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' import Notification from './layout/general/Notification' const IGNORE_CLUSTERING = new Set([ @@ -45,7 +46,7 @@ function Clustering({ category, children }) { const featureRef = React.useRef(null) const map = useMap() - const userCluster = useStore( + const userCluster = useStorage( (s) => s.userSettings[category]?.clustering || false, ) const { @@ -53,7 +54,7 @@ function Clustering({ category, children }) { clustering, general: { minZoom: configMinZoom }, }, - } = useStatic.getState() + } = useMemory.getState() const [rules] = React.useState( category in clustering diff --git a/src/components/Config.jsx b/src/components/Config.jsx index 6de1e545e..6dc15b306 100644 --- a/src/components/Config.jsx +++ b/src/components/Config.jsx @@ -2,7 +2,8 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { setUser } from '@sentry/react' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' import Fetch from '@services/Fetch' import { setLoadingText } from '@services/functions/setLoadingText' import Utility from '@services/Utility' @@ -41,7 +42,7 @@ export default function Config({ children }) { }) } - /** @type {{ state: import('@hooks/useStore').UseStore}} */ + /** @type {{ state: import('@hooks/useStorage').UseStorage}} */ const localState = JSON.parse( localStorage.getItem('local-state') || '{ "state": {} }', ) @@ -91,7 +92,7 @@ export default function Config({ children }) { useScannerSessionStorage.setState((prev) => ({ cooldown: Math.max(prev.cooldown, data.user.cooldown || 0), })) - useStatic.setState({ + useMemory.setState({ auth: { strategy: data.user?.strategy || '', discordId: data.user?.discordId || '', @@ -108,6 +109,7 @@ export default function Config({ children }) { : {}, counts: data.authReferences || {}, userBackupLimits: data.database.settings.userBackupLimits || 0, + excludeList: data.authentication.excludeList || [], }, theme: data.map.theme, ui: data.ui, @@ -125,7 +127,7 @@ export default function Config({ children }) { tutorialExcludeList: data.authentication.excludeFromTutorial || [], }) - useStore.setState((prev) => ({ + useStorage.setState((prev) => ({ tutorial: !localState?.state?.tutorial || data.user.tutorial === undefined ? !!localState?.state?.tutorial diff --git a/src/components/Container.jsx b/src/components/Container.jsx index 999e62de1..ca4320d83 100644 --- a/src/components/Container.jsx +++ b/src/components/Container.jsx @@ -1,14 +1,17 @@ +// @ts-check import * as React from 'react' import { MapContainer } from 'react-leaflet' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' +import { useMapStore } from '@hooks/useMapStore' import Utility from '@services/Utility' import Map from './Map' import ScanOnDemand from './layout/dialogs/scanner/ScanOnDemand' import DraggableMarker from './layout/dialogs/webhooks/human/Draggable' import WebhookAreaSelection from './layout/dialogs/webhooks/human/area/AreaSelection' -import Nav from './layout/Nav' +import { Nav } from './layout/Nav' import ActiveWeather from './layout/general/ActiveWeather' import { ControlledLocate, @@ -17,41 +20,38 @@ import { } from './Layers' import { Effects } from './Effects' -/** @param {{ target: import('leaflet').Map, type: string }} */ +/** @param {{ target: import('leaflet').Map, type: string }} args */ function setLocationZoom({ target: map }) { const { lat, lng } = map.getCenter() const zoom = map.getZoom() - useStore.setState({ location: [lat, lng], zoom }) - useStatic.setState({ + useStorage.setState({ location: [lat, lng], zoom }) + useMemory.setState({ timeOfDay: Utility.timeCheck(lat, lng), }) if (map.hasEventListeners('fetchdata')) map.fire('fetchdata') } -const MAX_BOUNDS = [ +const MAX_BOUNDS = /** @type {[[number, number], [number, number]]} */ ([ [-90, -210], [90, 210], -] +]) export default function Container() { - const { location, zoom } = useStore.getState() + const { location, zoom } = useStorage.getState() return ( - useStatic.setState((prev) => { - if (ref) { - ref.attributionControl.setPrefix( - prev.config.general.attributionPrefix || '', - ) - ref.on('moveend', setLocationZoom) - ref.on('zoomend', setLocationZoom) - } - return { map: ref } - }) - } + ref={(ref) => { + if (ref) { + const { attributionPrefix } = useMemory.getState().config.general + ref.attributionControl.setPrefix(attributionPrefix || '') + ref.on('moveend', setLocationZoom) + ref.on('zoomend', setLocationZoom) + useMapStore.setState({ map: ref }) + } + }} zoom={zoom} zoomControl={false} maxBounds={MAX_BOUNDS} diff --git a/src/components/Effects.jsx b/src/components/Effects.jsx index f97fbcc0c..e48e8ec32 100644 --- a/src/components/Effects.jsx +++ b/src/components/Effects.jsx @@ -3,7 +3,7 @@ import useMediaQuery from '@mui/material/useMediaQuery' import useGenerate from '@hooks/useGenerate' import useRefresh from '@hooks/useRefresh' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' import { useParams } from 'react-router-dom' import { useMap } from 'react-leaflet' import { useTranslation } from 'react-i18next' @@ -18,10 +18,10 @@ export function Effects() { const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs')) const isTablet = useMediaQuery((theme) => theme.breakpoints.only('sm')) - const online = useStatic((s) => s.online) + const online = useMemory((s) => s.online) React.useEffect(() => { - useStatic.setState({ isMobile, isTablet }) + useMemory.setState({ isMobile, isTablet }) }, [isMobile, isTablet]) React.useEffect(() => { @@ -36,7 +36,7 @@ export function Effects() { React.useEffect(() => { map.attributionControl.setPrefix( online - ? useStatic.getState().config.general.attributionPrefix || '' + ? useMemory.getState().config.general.attributionPrefix || '' : t('offline_mode'), ) }, [online]) diff --git a/src/components/HolidayEffects.jsx b/src/components/HolidayEffects.jsx index 37f80edc4..a0e6be2cc 100644 --- a/src/components/HolidayEffects.jsx +++ b/src/components/HolidayEffects.jsx @@ -1,7 +1,8 @@ // @ts-check import * as React from 'react' import HolidayAnimations from '@services/HolidayAnimations' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' /** * @@ -12,7 +13,7 @@ export function HolidayEffect({ images, name, css, imageScale }) { const [element, setElement] = React.useState( /** @type {React.ReactNode} */ (null), ) - const userDisabled = useStore((s) => s.holidayEffects[name] === true) + const userDisabled = useStorage((s) => s.holidayEffects[name] === true) React.useLayoutEffect(() => { if (userDisabled) { @@ -56,7 +57,7 @@ export function HolidayEffect({ images, name, css, imageScale }) { } export default function HolidayEffects() { - const holidayEffects = useStatic((s) => s?.config?.holidayEffects || []) + const holidayEffects = useMemory((s) => s?.config?.holidayEffects || []) return ( <> diff --git a/src/components/Layers.jsx b/src/components/Layers.jsx index c4f5185cb..bd8a5cd07 100644 --- a/src/components/Layers.jsx +++ b/src/components/Layers.jsx @@ -1,9 +1,9 @@ // @ts-check import * as React from 'react' -import { TileLayer, ZoomControl, useMap } from 'react-leaflet' +import { TileLayer, useMap } from 'react-leaflet' import { control } from 'leaflet' -import { useStore } from '@hooks/useStore' +import { useStorage } from '@hooks/useStorage' import useTileLayer from '@hooks/useTileLayer' export function ControlledTileLayer() { @@ -12,15 +12,26 @@ export function ControlledTileLayer() { } export function ControlledZoomLayer() { - const navSetting = useStore( + const map = useMap() + const navSetting = useStorage( (state) => state.settings.navigationControls === 'leaflet', ) - return navSetting ? : null + + React.useLayoutEffect(() => { + if (navSetting) { + const zoom = control.zoom({ position: 'bottomright' }).addTo(map) + return () => { + zoom.remove() + } + } + }, [navSetting]) + + return null } export function ControlledLocate() { const map = useMap() - const navSetting = useStore( + const navSetting = useStorage( (state) => state.settings.navigationControls === 'leaflet', ) diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 0aff0ecfb..ef882500a 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -1,18 +1,22 @@ // @ts-check import * as React from 'react' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' +import { useMapStore } from '@hooks/useMapStore' import Utility from '@services/Utility' + import FilterPermCheck from './QueryData' export default function Map() { Utility.analytics(window.location.pathname) - const ready = useStatic((s) => !!s.map && !!s.Icons) - const ui = useStatic((s) => s.ui) - const profiling = useStore((s) => s.profiling) + const iconsReady = useMemory((s) => !!s.Icons) + const mapReady = useMapStore((s) => !!s.map) + const ui = useMemory((s) => s.ui) + const profiling = useStorage((s) => s.profiling) - if (!ready) return null + if (!iconsReady || !mapReady) return null return ( <> {Object.keys({ ...ui, ...ui.wayfarer, ...ui.admin }).map((category) => { diff --git a/src/components/QueryData.jsx b/src/components/QueryData.jsx index 9c4234b22..e1dd1ec2c 100644 --- a/src/components/QueryData.jsx +++ b/src/components/QueryData.jsx @@ -3,20 +3,20 @@ import * as React from 'react' import { useQuery } from '@apollo/client' import { useMap } from 'react-leaflet' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' import { usePermCheck } from '@hooks/usePermCheck' import Query from '@services/Query' import { getQueryArgs } from '@services/functions/getQueryArgs' import RobustTimeout from '@services/apollo/RobustTimeout' import Utility from '@services/Utility' +import { FILTER_SKIP_LIST } from '@assets/constants' import * as index from './tiles/index' import Clustering from './Clustering' import Notification from './layout/general/Notification' import { GenerateCells } from './tiles/S2Cell' -const FILTER_SKIP_LIST = ['filter', 'enabled', 'legacy'] - /** @param {string} category */ const userSettingsCategory = (category) => { switch (category) { @@ -32,11 +32,20 @@ const userSettingsCategory = (category) => { } } +/** + * @template {keyof import('@rm/types').AllFilters} T + * @param {import('@rm/types').AllFilters[T]} requestedFilters + * @param {Record} userSettings + * @param {T} category + * @param {string[]} [onlyAreas] + * @returns + */ const trimFilters = (requestedFilters, userSettings, category, onlyAreas) => { - const { filters: staticFilters } = useStatic.getState() + const { filters: staticFilters } = useMemory.getState() + const easyMode = !!requestedFilters?.easyMode const trimmed = { onlyLegacy: userSettings?.legacyFilter, - onlyLinkGlobal: userSettings?.linkGlobalAndAdvanced, + onlyLinkGlobal: userSettings?.linkGlobalAndAdvanced || easyMode, onlyAllPvp: userSettings?.showAllPvpRanks, onlyAreas: onlyAreas || [], } @@ -53,11 +62,14 @@ const trimFilters = (requestedFilters, userSettings, category, onlyAreas) => { entryV } }) - Object.entries(requestedFilters.filter).forEach((filter) => { - const [id, specifics] = filter + Object.entries(requestedFilters.filter).forEach(([id, specifics]) => { + // eslint-disable-next-line no-unused-vars + const { enabled, size, ...rest } = (easyMode + ? requestedFilters.ivOr + : specifics) || { all: false, adv: '' } if (specifics && specifics.enabled && staticFilters[category]?.filter[id]) { - trimmed[id] = specifics + trimmed[id] = rest } }) return trimmed @@ -65,7 +77,7 @@ const trimFilters = (requestedFilters, userSettings, category, onlyAreas) => { export default function FilterPermCheck({ category }) { const valid = usePermCheck(category) - const error = useStatic((state) => state.clientError) + const error = useMemory((state) => state.clientError) if (!valid || error) { return null @@ -89,14 +101,14 @@ function QueryData({ category, timeout }) { const map = useMap() - const hideList = useStatic((s) => s.hideList) - const active = useStatic((s) => s.active) + const hideList = useMemory((s) => s.hideList) + const active = useMemory((s) => s.active) - const userSettings = useStore( + const userSettings = useStorage( (s) => s.userSettings[userSettingsCategory(category)], ) - const filters = useStore((s) => s.filters[category]) - const onlyAreas = useStore( + const filters = useStorage((s) => s.filters[category]) + const onlyAreas = useStorage( (s) => s.filters?.scanAreas?.filterByAreas && s.filters?.scanAreas?.filter?.areas, @@ -129,11 +141,13 @@ function QueryData({ category, timeout }) { if (active) { timeout.current.setupTimeout(refetch) return () => { - useStatic.setState({ excludeList: [] }) + useMemory.setState((prev) => ({ + excludeList: prev.excludeList.length ? [] : prev.excludeList, + })) timeout.current.off() } } - }, [active, refetch, timeout]) + }, [active, refetch, timeout.current]) React.useEffect(() => { const refetchData = () => { @@ -149,17 +163,17 @@ function QueryData({ category, timeout }) { return () => { map.off('fetchdata', refetchData) } - }, [filters, userSettings, onlyAreas]) + }, [filters, userSettings, onlyAreas, timeout.current.refetch]) if (error) { // @ts-ignore if (error.networkError?.statusCode === 464) { - useStatic.setState({ clientError: 'old_client' }) + useMemory.setState({ clientError: 'old_client' }) return null } // @ts-ignore if (error.networkError?.statusCode === 511) { - useStatic.setState({ clientError: 'session_expired' }) + useMemory.setState({ clientError: 'session_expired' }) return null } } diff --git a/src/components/WebhookQuery.jsx b/src/components/WebhookQuery.jsx index 5973d2097..9357de6c8 100644 --- a/src/components/WebhookQuery.jsx +++ b/src/components/WebhookQuery.jsx @@ -4,7 +4,8 @@ import { useParams } from 'react-router-dom' import { useQuery } from '@apollo/client' import Query from '@services/Query' -import { useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useStorage } from '@hooks/useStorage' /** * @param {string} category @@ -46,14 +47,14 @@ export default function WebhookQuery({ children }) { data?.[`${lowercase}Single`]?.lat && data[`${lowercase}Single`]?.lon ) { - useStore.setState((prev) => ({ + useStorage.setState((prev) => ({ location: [ data[`${lowercase}Single`].lat, data[`${lowercase}Single`].lon, ], zoom: +params.zoom || prev.zoom, })) - useStatic.setState({ + useMemory.setState({ manualParams: { category: params.category, id: params.id, diff --git a/src/components/layout/FloatingBtn.jsx b/src/components/layout/FloatingBtn.jsx index 570e5e75c..3a70d06e2 100644 --- a/src/components/layout/FloatingBtn.jsx +++ b/src/components/layout/FloatingBtn.jsx @@ -26,7 +26,9 @@ import { DomEvent } from 'leaflet' import { FAB_BUTTONS } from '@services/queries/config' import useLocation from '@hooks/useLocation' -import { useLayoutStore, useStatic, useStore } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' +import { useStorage } from '@hooks/useStorage' import { useScanStore } from './dialogs/scanner/store' import { I } from './general/I' @@ -65,7 +67,7 @@ const handleClick = (name) => () => { } } -export default function FloatingButtons() { +export function FloatingButtons() { const { t } = useTranslation() const { data } = useQuery(FAB_BUTTONS, { fetchPolicy: 'cache-first', @@ -73,12 +75,12 @@ export default function FloatingButtons() { const map = useMap() const { lc, color } = useLocation() - const reactControls = useStore( + const reactControls = useStorage( (s) => s.settings.navigationControls === 'react', ) - const isMobile = useStatic((s) => s.isMobile) - const online = useStatic((s) => s.online) + const isMobile = useMemory((s) => s.isMobile) + const online = useMemory((s) => s.online) const webhookMode = useWebhookStore((s) => s.mode) @@ -258,3 +260,5 @@ export default function FloatingButtons() { ) } + +export const FloatingButtonsMemo = React.memo(FloatingButtons, () => true) diff --git a/src/components/layout/Nav.jsx b/src/components/layout/Nav.jsx index cc8d0d188..e02f48568 100644 --- a/src/components/layout/Nav.jsx +++ b/src/components/layout/Nav.jsx @@ -1,13 +1,13 @@ import * as React from 'react' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' -import FloatingBtn from './FloatingBtn' +import { FloatingButtonsMemo } from './FloatingBtn' import Sidebar from './drawer/Drawer' import FilterMenu from './dialogs/filters/FilterMenu' import UserOptions from './dialogs/UserOptions' import Tutorial from './dialogs/tutorial/Tutorial' -import UserProfile from './dialogs/UserProfile' +import UserProfile from './dialogs/profile' import Search from './dialogs/Search' import MessageOfTheDay from './dialogs/Motd' import DonorPage from './dialogs/DonorPage' @@ -17,27 +17,40 @@ import ScanDialog from './dialogs/scanner/ScanDialog' import Webhook from './dialogs/webhooks/Webhook' import ClientError from './dialogs/ClientError' import { WebhookNotification } from './dialogs/webhooks/Notification' +import AdvancedFilter from './dialogs/filters/Advanced' +import BadgeSelection from './dialogs/BadgeSelection' +import WebhookAdvanced from './dialogs/webhooks/WebhookAdv' +import SlotSelection from './dialogs/filters/SlotSelection' +import { HelpDialog } from './dialogs/Help' -export default function Nav() { - const iconsIsReady = useStatic((s) => !!s.Icons) - if (!iconsIsReady) return null - return ( - <> - - - - - - - - - - - - - - - - - ) -} +export const Nav = React.memo( + () => { + const iconsIsReady = useMemory((s) => !!s.Icons) + if (!iconsIsReady) return null + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ) + }, + () => true, +) diff --git a/src/components/layout/auth/Auth.jsx b/src/components/layout/auth/Auth.jsx index ae7b6a4c4..61ea8175a 100644 --- a/src/components/layout/auth/Auth.jsx +++ b/src/components/layout/auth/Auth.jsx @@ -2,14 +2,14 @@ import * as React from 'react' import { Navigate, useParams } from 'react-router-dom' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' import Container from '../../Container' import WebhookQuery from '../../WebhookQuery' export default function Auth() { const params = useParams() - const mapPerm = useStatic((s) => s.auth.perms.map) + const mapPerm = useMemory((s) => s.auth.perms.map) if (!mapPerm) { if ((params.category && params.id) || (params.lat && params.lon)) { diff --git a/src/components/layout/auth/Blocked.jsx b/src/components/layout/auth/Blocked.jsx index ffd1f7bf0..b30ceb83c 100644 --- a/src/components/layout/auth/Blocked.jsx +++ b/src/components/layout/auth/Blocked.jsx @@ -15,7 +15,7 @@ import ListItem from '@mui/material/ListItem' import ListItemIcon from '@mui/material/ListItemIcon' import ListItemText from '@mui/material/ListItemText' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' import DiscordLogin from './Discord' import ThemeToggle from '../general/ThemeToggle' @@ -25,7 +25,7 @@ export default function Blocked() { const { t } = useTranslation() const { info } = useParams() const navigate = useNavigate() - const discordInvite = useStatic((s) => s.config?.links?.discordInvite) + const discordInvite = useMemory((s) => s.config?.links?.discordInvite) const queryParams = new URLSearchParams(info) const blockedGuilds = queryParams.get('blockedGuilds') const username = queryParams.get('username') diff --git a/src/components/layout/auth/Login.jsx b/src/components/layout/auth/Login.jsx index e51113791..63de81002 100644 --- a/src/components/layout/auth/Login.jsx +++ b/src/components/layout/auth/Login.jsx @@ -10,7 +10,7 @@ import { useQuery } from '@apollo/client' import { CUSTOM_COMPONENT } from '@services/queries/config' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' import LocalLogin from './Local' import LocaleSelection from '../general/LocaleSelection' import DiscordLogin from './Discord' @@ -20,19 +20,19 @@ import ThemeToggle from '../general/ThemeToggle' import { Loading } from '../general/Loading' export default function Login() { - const loggedIn = useStatic((s) => s.auth.loggedIn) - const loginPage = useStatic((s) => !!s.config.loginPage) - const headerTitle = useStatic((s) => s.config.general.headerTitle) - const discordInvite = useStatic((s) => s.config.links.discordInvite) - const discordAuthUrl = useStatic((s) => s.config.customRoutes.discordAuthUrl) - const telegramBotName = useStatic( + const loggedIn = useMemory((s) => s.auth.loggedIn) + const loginPage = useMemory((s) => !!s.config.loginPage) + const headerTitle = useMemory((s) => s.config.general.headerTitle) + const discordInvite = useMemory((s) => s.config.links.discordInvite) + const discordAuthUrl = useMemory((s) => s.config.customRoutes.discordAuthUrl) + const telegramBotName = useMemory( (s) => s.config.customRoutes.telegramBotName, ) - const telegramAuthUrl = useStatic( + const telegramAuthUrl = useMemory( (s) => s.config.customRoutes.telegramAuthUrl, ) - const localAuthUrl = useStatic((s) => s.config.customRoutes.localAuthUrl) - const authMethods = useStatic((s) => s.auth.methods) + const localAuthUrl = useMemory((s) => s.config.customRoutes.localAuthUrl) + const authMethods = useMemory((s) => s.auth.methods) const { t, i18n } = useTranslation() const { data, loading } = useQuery(CUSTOM_COMPONENT, { diff --git a/src/components/layout/custom/Generator.jsx b/src/components/layout/custom/Generator.jsx index 5e88c815d..1ed6f3b1b 100644 --- a/src/components/layout/custom/Generator.jsx +++ b/src/components/layout/custom/Generator.jsx @@ -10,7 +10,7 @@ import LocalLogin from '../auth/Local' import Telegram from '../auth/Telegram' import CustomText from './CustomText' import CustomButton from './CustomButton' -import { Img } from './CustomImg' +import { Img } from '../general/Img' import LocaleSelection from '../general/LocaleSelection' import LinkWrapper from './LinkWrapper' diff --git a/src/components/layout/dialogs/BadgeSelection.jsx b/src/components/layout/dialogs/BadgeSelection.jsx index 4d749b599..239e73902 100644 --- a/src/components/layout/dialogs/BadgeSelection.jsx +++ b/src/components/layout/dialogs/BadgeSelection.jsx @@ -1,69 +1,77 @@ // @ts-check import * as React from 'react' import DialogContent from '@mui/material/DialogContent' -import Button from '@mui/material/Button' -import ButtonGroup from '@mui/material/ButtonGroup' -import { useTranslation } from 'react-i18next' +import Dialog from '@mui/material/Dialog' import { useMutation } from '@apollo/client' import { apolloClient, apolloCache } from '@services/apollo' import Query from '@services/Query' +import { ENUM_BADGES } from '@assets/constants' +import { useLayoutStore } from '@hooks/useLayoutStore' import Header from '../general/Header' import Footer from '../general/Footer' +import { MultiSelector } from '../drawer/MultiSelector' -export default function BadgeSelection({ id, setBadgeMenu, badge }) { - const { t } = useTranslation() +const handleClose = () => + useLayoutStore.setState({ + gymBadge: { + open: false, + gymId: '', + badge: 0, + }, + }) + +const footerOptions = + /** @type {import('../general/Footer').FooterButton[]} */ ([ + { + name: 'close', + action: handleClose, + color: 'primary', + align: 'right', + }, + ]) +export default function BadgeSelection() { + const { gymId, badge, open } = useLayoutStore((s) => s.gymBadge) const [setBadgeInDb] = useMutation(Query.user('setGymBadge'), { refetchQueries: ['GetBadgeInfo'], }) + /** @type {import('packages/types/lib').MultiSelectorProps['onClick']} */ + const onClick = React.useCallback( + (_, newV) => () => { + setBadgeInDb({ + variables: { + badge: newV, + gymId, + }, + }) + apolloClient.cache.modify({ + id: apolloCache.identify({ __typename: 'Gym', id: gymId }), + fields: { + badge() { + return newV + }, + }, + }) + handleClose() + }, + [setBadgeInDb, gymId], + ) + return ( - <> -

setBadgeMenu(false)} /> - - - {[0, 1, 2, 3].map((i) => ( - - ))} - + +
+ + -
setBadgeMenu(false), - color: 'primary', - align: 'right', - }, - ]} - role="webhook_footer" - /> - +
+
) } diff --git a/src/components/layout/dialogs/ClientError.jsx b/src/components/layout/dialogs/ClientError.jsx index d1cf06b73..80aac0c22 100644 --- a/src/components/layout/dialogs/ClientError.jsx +++ b/src/components/layout/dialogs/ClientError.jsx @@ -7,17 +7,17 @@ import Typography from '@mui/material/Typography' import Button from '@mui/material/Button' import { useTranslation } from 'react-i18next' -import { useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' import Header from '../general/Header' export default function ClientError() { const { t } = useTranslation() - const error = useStatic((s) => s.clientError) + const error = useMemory((s) => s.clientError) return ( -
+

{t(`${error}_body`)} diff --git a/src/components/layout/dialogs/DialogWrapper.jsx b/src/components/layout/dialogs/DialogWrapper.jsx index 6378307e2..5f2e1ed60 100644 --- a/src/components/layout/dialogs/DialogWrapper.jsx +++ b/src/components/layout/dialogs/DialogWrapper.jsx @@ -1,15 +1,16 @@ -/* eslint-disable no-nested-ternary */ // @ts-check import * as React from 'react' import Dialog from '@mui/material/Dialog' -import { useLayoutStore, useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' /** * * @param {{ - * dialog: keyof ReturnType, + * dialog?: keyof ReturnType, * variant?: 'small' | 'large' * children: React.ReactNode + * open?: boolean * } & Omit} props * @returns {JSX.Element} */ @@ -21,7 +22,7 @@ export function DialogWrapper({ ...props }) { const open = useLayoutStore((s) => s[dialog]) - const isMobile = useStatic((s) => s.isMobile) + const isMobile = useMemory((s) => s.isMobile) const handleClose = React.useCallback( () => useLayoutStore.setState({ [dialog]: false }), diff --git a/src/components/layout/dialogs/DonorPage.jsx b/src/components/layout/dialogs/DonorPage.jsx index d1200023c..10f840330 100644 --- a/src/components/layout/dialogs/DonorPage.jsx +++ b/src/components/layout/dialogs/DonorPage.jsx @@ -5,7 +5,8 @@ import { useQuery } from '@apollo/client' import Dialog from '@mui/material/Dialog' import { CUSTOM_COMPONENT } from '@services/queries/config' -import { useLayoutStore, useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' import DialogWrapper from '../custom/DialogWrapper' import CustomTile from '../custom/CustomTile' @@ -22,7 +23,7 @@ const handleClose = () => useLayoutStore.setState({ donorPage: false }) export default function DonorPage() { const open = useLayoutStore((s) => s.donorPage) - const isMobile = useStatic((s) => s.isMobile) + const isMobile = useMemory((s) => s.isMobile) const { data, loading } = useQuery(CUSTOM_COMPONENT, { fetchPolicy: 'cache-first', diff --git a/src/components/layout/dialogs/Feedback.jsx b/src/components/layout/dialogs/Feedback.jsx index 7d23e9679..515de03c8 100644 --- a/src/components/layout/dialogs/Feedback.jsx +++ b/src/components/layout/dialogs/Feedback.jsx @@ -7,7 +7,8 @@ import Button from '@mui/material/Button' import Divider from '@mui/material/Divider' import { useTranslation } from 'react-i18next' -import { useLayoutStore, useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' import Header from '../general/Header' import Footer from '../general/Footer' @@ -16,7 +17,7 @@ import { DialogWrapper } from './DialogWrapper' export default function Feedback() { const { t } = useTranslation() - const link = useStatic((s) => s.config.links.feedbackLink) + const link = useMemory((s) => s.config.links.feedbackLink) const handleClose = React.useCallback( () => useLayoutStore.setState({ feedback: false }), [], @@ -24,7 +25,7 @@ export default function Feedback() { return ( -
+
{t('use_the_link_below')} diff --git a/src/components/layout/dialogs/Help.jsx b/src/components/layout/dialogs/Help.jsx new file mode 100644 index 000000000..85bbc433e --- /dev/null +++ b/src/components/layout/dialogs/Help.jsx @@ -0,0 +1,18 @@ +// @ts-check +import * as React from 'react' + +import { useLayoutStore } from '@hooks/useLayoutStore' + +import Help from './tutorial/Advanced' +import { DialogWrapper } from './DialogWrapper' + +export function HelpDialog() { + const { open, category } = useLayoutStore((s) => s.help) + const handleClose = () => + useLayoutStore.setState({ help: { open: false, category: '' } }) + return ( + + + + ) +} diff --git a/src/components/layout/dialogs/Motd.jsx b/src/components/layout/dialogs/Motd.jsx index f47ee7862..af758a928 100644 --- a/src/components/layout/dialogs/Motd.jsx +++ b/src/components/layout/dialogs/Motd.jsx @@ -6,7 +6,8 @@ import Dialog from '@mui/material/Dialog' import Box from '@mui/material/Box' import { useQuery } from '@apollo/client' -import { useLayoutStore, useStore } from '@hooks/useStore' +import { useLayoutStore } from '@hooks/useLayoutStore' +import { useStorage } from '@hooks/useStorage' import { CUSTOM_COMPONENT, MOTD_CHECK } from '@services/queries/config' import Utility from '@services/Utility' @@ -14,16 +15,18 @@ import DialogWrapper from '../custom/DialogWrapper' import CustomTile from '../custom/CustomTile' import { Loading } from '../general/Loading' -const DEFAULT = { - settings: {}, - components: [], - titles: [], - footerButtons: [], -} +const DEFAULT = + /** @type {import('@rm/types').Config['map']['messageOfTheDay']} */ ({ + settings: {}, + components: [], + titles: [], + footerButtons: [], + index: 0, + }) export default function MessageOfTheDay() { - const clientIndex = useStore((s) => s.motdIndex) - const tutorial = useStore((s) => s.tutorial) + const clientIndex = useStorage((s) => s.motdIndex) + const tutorial = useStorage((s) => s.tutorial) const open = useLayoutStore((s) => s.motd) @@ -42,7 +45,7 @@ export default function MessageOfTheDay() { const handleMotdClose = React.useCallback(() => { if (motd.settings.permanent === false) { - useStore.setState({ motdIndex: motd.settings.index }) + useStorage.setState({ motdIndex: motd.index }) } useLayoutStore.setState({ motd: false }) }, [motd]) diff --git a/src/components/layout/dialogs/NestSubmission.jsx b/src/components/layout/dialogs/NestSubmission.jsx index 7b2812dae..ad33de4e2 100644 --- a/src/components/layout/dialogs/NestSubmission.jsx +++ b/src/components/layout/dialogs/NestSubmission.jsx @@ -6,7 +6,8 @@ import { useMutation } from '@apollo/client' import { useTranslation } from 'react-i18next' import Query from '@services/Query' -import { useLayoutStore, useStatic } from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { useLayoutStore } from '@hooks/useLayoutStore' import Header from '../general/Header' import Footer from '../general/Footer' @@ -43,7 +44,7 @@ export default function NestSubmission({ id, name }) { React.useEffect(() => { if (error) { - useStatic.setState({ + useMemory.setState({ webhookAlert: { open: true, severity: 'error', @@ -58,7 +59,7 @@ export default function NestSubmission({ id, name }) { return ( -
+
s.darkMode) - const Icons = useStatic((s) => s.Icons) + const darkMode = useStorage((s) => s.darkMode) + const Icons = useMemory((s) => s.Icons) return ( s.searchLoading) + const loading = useMemory((s) => s.searchLoading) return ( <> @@ -84,7 +86,7 @@ function EndAdornment({ children }) { export function FancySearch({ InputProps, ...props }) { const { t } = useTranslation() - const searchTab = useStore((s) => s.searchTab) + const searchTab = useStorage((s) => s.searchTab) const { data } = useQuery(SEARCHABLE, { fetchPolicy: 'cache-first', @@ -94,7 +96,7 @@ export function FancySearch({ InputProps, ...props }) { const handleClose = React.useCallback((selection) => { if (typeof selection === 'string') { - useStore.setState({ searchTab: selection }) + useStorage.setState({ searchTab: selection }) } setAnchorEl(null) }, []) @@ -105,7 +107,7 @@ export function FancySearch({ InputProps, ...props }) { (typeof searchTab === 'number' || !data.searchable.includes(searchTab)) ) { // searchTab value migration - useStore.setState({ searchTab: data?.searchable[0] }) + useStorage.setState({ searchTab: data?.searchable[0] }) } }, [searchTab, data]) @@ -180,12 +182,12 @@ export default function Search() { const { t, i18n } = useTranslation() const map = useMap() - const search = useStore((state) => state.search) - const searchTab = useStore((state) => state.searchTab) + const search = useStorage((state) => state.search) + const searchTab = useStorage((state) => state.searchTab) - const distanceUnit = useStatic((state) => state.config.misc.distanceUnit) - const questMessage = useStatic((state) => state.config.misc.questMessage) - const isMobile = useStatic((s) => s.isMobile) + const distanceUnit = useMemory((state) => state.config.misc.distanceUnit) + const questMessage = useMemory((state) => state.config.misc.questMessage) + const isMobile = useMemory((s) => s.isMobile) const open = useLayoutStore((s) => s.search) @@ -199,7 +201,7 @@ export default function Search() { useLayoutStore.setState({ search: false }) if (typeof result === 'object' && 'lat' in result && 'lon' in result) { map.flyTo([result.lat, result.lon], 16) - useStatic.setState({ + useMemory.setState({ manualParams: { category: fromSearchCategory(searchTab), id: result.id, @@ -211,7 +213,7 @@ export default function Search() { const sendToServer = React.useCallback( (/** @type {string} */ newSearch) => { const { lat, lng } = map.getCenter() - const { areas } = useStore.getState().filters.scanAreas?.filter || { + const { areas } = useStorage.getState().filters.scanAreas?.filter || { areas: [], } callSearch({ @@ -239,7 +241,7 @@ export default function Search() { e?.type === 'change' && (/^[0-9\s\p{L}]+$/u.test(newValue) || newValue === '') ) { - useStore.setState({ search: newValue.toLowerCase() }) + useStorage.setState({ search: newValue.toLowerCase() }) debounceChange(newValue.toLowerCase()) } }, @@ -262,7 +264,7 @@ export default function Search() { }, [data]) React.useEffect(() => { - useStatic.setState({ searchLoading: loading }) + useMemory.setState({ searchLoading: loading }) }, [loading]) React.useEffect(() => { @@ -286,7 +288,7 @@ export default function Search() { }} > -
+
s.Icons) + const Icons = useMemory((s) => s.Icons) if (props.url) { return ( diff --git a/src/components/layout/dialogs/UserOptions.jsx b/src/components/layout/dialogs/UserOptions.jsx index 5a3bd6989..61eea7742 100644 --- a/src/components/layout/dialogs/UserOptions.jsx +++ b/src/components/layout/dialogs/UserOptions.jsx @@ -10,12 +10,9 @@ import { import { useTranslation, Trans } from 'react-i18next' import Utility from '@services/Utility' -import { - useLayoutStore, - useStatic, - useStore, - toggleDialog, -} from '@hooks/useStore' +import { useMemory } from '@hooks/useMemory' +import { toggleDialog, useLayoutStore } from '@hooks/useLayoutStore' +import { useStorage } from '@hooks/useStorage' import { getPermission } from '@services/desktopNotification' import Header from '../general/Header' @@ -23,7 +20,7 @@ import Footer from '../general/Footer' import { DialogWrapper } from './DialogWrapper' function InputType({ option, subOption, localState, handleChange, category }) { - const staticUserSettings = useStatic.getState().userSettings[category] || {} + const staticUserSettings = useMemory.getState().userSettings[category] || {} const fullOption = subOption ? staticUserSettings[option].sub[subOption] : staticUserSettings[option] @@ -74,8 +71,8 @@ export default function UserOptions() { const { t } = useTranslation() const { open, category, type } = useLayoutStore((s) => s.dialog) - const staticUserSettings = useStatic((s) => s.userSettings[category] || {}) - const userSettings = useStore((state) => state.userSettings) + const staticUserSettings = useMemory((s) => s.userSettings[category] || {}) + const userSettings = useStorage((state) => state.userSettings) const [localState, setLocalState] = React.useState(userSettings[category]) @@ -102,9 +99,40 @@ export default function UserOptions() { return t(Utility.camelToSnake(label), Utility.getProperName(label)) } + const footerOptions = React.useMemo( + () => + /** @type {import('@components/layout/general/Footer').FooterButton[]} */ ([ + { + name: 'reset', + action: () => { + const newSettings = { ...userSettings[category] } + Object.entries(staticUserSettings || {}).forEach(([key, value]) => { + if (value.sub) { + Object.entries(value.sub).forEach(([subKey, subValue]) => { + newSettings[subKey] = subValue.value + }) + } else { + newSettings[key] = value.value + } + }) + setLocalState(newSettings) + }, + icon: 'Replay', + color: 'primary', + }, + { + name: 'save', + action: toggleDialog(false, category, 'options', localState), + icon: 'Save', + color: 'secondary', + }, + ]), + [category, userSettings, staticUserSettings, localState], + ) + React.useEffect(() => { setLocalState(userSettings[category]) - }, [category]) + }, [category, userSettings[category]]) return ( -