From d63123ef3b4443e654100cab6b09225e885f32cb Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:51:59 -0500 Subject: [PATCH 01/48] refactor: almost all filter related frontend code --- packages/locales/lib/human/en.json | 3 +- packages/types/lib/config.d.ts | 9 +- packages/types/lib/general.d.ts | 22 + packages/types/lib/server.d.ts | 13 + server/src/services/filters/builder/gym.js | 3 +- .../src/services/filters/builder/pokemon.js | 28 +- .../src/services/filters/builder/pokestop.js | 2 +- server/src/services/ui/primary.js | 141 ++-- src/assets/constants.js | 37 + .../layout/dialogs/filters/Advanced.jsx | 429 +++++------- .../layout/dialogs/filters/Gender.jsx | 45 +- .../dialogs/filters/QuestConditions.jsx | 107 +++ .../layout/dialogs/filters/Size.jsx | 38 +- .../layout/dialogs/filters/SliderTile.jsx | 233 ++++--- .../layout/dialogs/filters/StringFilter.jsx | 103 +-- .../layout/dialogs/tutorial/Sliders.jsx | 2 +- .../layout/dialogs/webhooks/WebhookAdv.jsx | 2 +- src/components/layout/drawer/Areas.jsx | 4 +- src/components/layout/drawer/BoolToggle.jsx | 80 ++- .../layout/drawer/CollapsibleItem.jsx | 8 +- src/components/layout/drawer/Extras.jsx | 639 +++++++++--------- src/components/layout/drawer/ItemSearch.jsx | 55 +- .../layout/drawer/MultiSelector.jsx | 52 +- src/components/layout/drawer/Pokemon.jsx | 256 ++----- src/components/layout/drawer/Section.jsx | 9 +- src/components/layout/drawer/Settings.jsx | 6 +- src/components/layout/general/Footer.jsx | 31 +- src/components/layout/general/Header.jsx | 12 +- src/components/layout/general/Menu.jsx | 10 +- src/components/layout/general/TabPanel.jsx | 52 +- .../layout/general/ToggleTypography.jsx | 10 + src/hooks/useStore.js | 46 +- src/services/functions/setDeep.js | 16 +- 33 files changed, 1321 insertions(+), 1182 deletions(-) create mode 100644 src/assets/constants.js create mode 100644 src/components/layout/dialogs/filters/QuestConditions.jsx create mode 100644 src/components/layout/general/ToggleTypography.jsx diff --git a/packages/locales/lib/human/en.json b/packages/locales/lib/human/en.json index 3a8fbe5cc..a41692dfd 100644 --- a/packages/locales/lib/human/en.json +++ b/packages/locales/lib/human/en.json @@ -713,5 +713,6 @@ "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" + "expert_description": "Manual input queries for the most customization", + "icon_size": "Icon Size" } diff --git a/packages/types/lib/config.d.ts b/packages/types/lib/config.d.ts index 9ecd613ac..feeb2912b 100644 --- a/packages/types/lib/config.d.ts +++ b/packages/types/lib/config.d.ts @@ -163,7 +163,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 @@ -175,7 +175,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 @@ -223,4 +226,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..a014618f8 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,24 @@ export type UAssetsClient = Config['icons']['styles'][number] & { data: UICONS } export type FullClientIcons = Omit & { styles: (Config['icons']['styles'][number] & { data: UICONS })[] } + +export interface RMSliderProps 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[], + low?: number, + high?: number, +) => void diff --git a/packages/types/lib/server.d.ts b/packages/types/lib/server.d.ts index ad9109077..d474cef5b 100644 --- a/packages/types/lib/server.d.ts +++ b/packages/types/lib/server.d.ts @@ -235,3 +235,16 @@ 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 + +type PokemonFilter = AllFilters['pokemon'] 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/ui/primary.js b/server/src/services/ui/primary.js index a62315dce..e8112ab6a 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').RMSliderProps} 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({ diff --git a/src/assets/constants.js b/src/assets/constants.js new file mode 100644 index 000000000..36f174a09 --- /dev/null +++ b/src/assets/constants.js @@ -0,0 +1,37 @@ +export const ICON_SIZES = /** @type {const} */ (['sm', 'md', 'lg', 'xl']) + +export const FILTER_SIZES = /** @type {const} */ (['xxs', 'xxl']) + +export const IV_OVERRIDES = /** @type {const} */ (['zeroIv', 'hundoIv']) + +export const GENDERS = /** @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 SPAWNPOINT_TTH = /** @type {const} */ ([0, 1, 2]) + +export const SLIDER_LABELS = /** @type {const} */ (['min', 'max']) diff --git a/src/components/layout/dialogs/filters/Advanced.jsx b/src/components/layout/dialogs/filters/Advanced.jsx index 96e7713c2..f9c8ac8f6 100644 --- a/src/components/layout/dialogs/filters/Advanced.jsx +++ b/src/components/layout/dialogs/filters/Advanced.jsx @@ -1,282 +1,223 @@ -import React, { Fragment, useState, useEffect } from 'react' -import { - Select, - Typography, - Grid, - DialogContent, - MenuItem, - Switch, - FormControl, - InputLabel, -} from '@mui/material' +// @ts-check +import * as React from 'react' +import { DialogContent, List, ListItem, Dialog } from '@mui/material' +import Grid2 from '@mui/material/Unstable_Grid2/Grid2' import { useTranslation } from 'react-i18next' import Utility from '@services/Utility' -import { useStore, useStatic } from '@hooks/useStore' - +import { useStore, useStatic, useDeepStore } from '@hooks/useStore' import Header from '@components/layout/general/Header' import Footer from '@components/layout/general/Footer' -import StringFilter from './StringFilter' +import { + BoolToggle, + DualBoolToggle, +} from '@components/layout/drawer/BoolToggle' + +import { StringFilter } from './StringFilter' import SliderTile from './SliderTile' import Size from './Size' -import QuestTitle from '../../general/QuestTitle' -import GenderFilter from './Gender' +import { GenderListItem } from './Gender' +import { QuestConditionSelector } from './QuestConditions' + +const STANDARD_BACKUP = /** @type {import('@rm/types/lib').BaseFilter} */ ({ + enabled: false, + size: 'md', + all: false, + adv: '', +}) +/** + * + * @param {{ + * id: string, + * category: 'pokemon' | 'gyms' | 'pokestops' | 'nests', + * open: boolean, + * setOpen: (open: boolean) => void + * selectedIds?: string[] + * }} props + * @returns + */ export default function AdvancedFilter({ - toggleAdvMenu, - advancedFilter, - type, - isMobile, + id, + category, + open, + setOpen, + selectedIds, }) { - Utility.analytics(`/${type}/${advancedFilter.id}`) - - const ui = useStatic((state) => state.ui) - const { questConditions } = useStatic((state) => state.available) - const [filterValues, setFilterValues] = useState(advancedFilter.tempFilters) - const filters = useStore((state) => state.filters) - const userSettings = useStore((state) => state.userSettings) + Utility.analytics(`/${category}/${id}`) const { t } = useTranslation() + const ui = useStatic((s) => s.ui[category]) + const isMobile = useStatic((s) => s.isMobile) + + const legacyFilter = useStore( + (s) => s.userSettings[category]?.legacyFilter || false, + ) + const [filters, setFilters] = useDeepStore(`filters.${category}.filter.${id}`) + const standard = useStore((s) => + category === 'pokemon' ? s.filters[category].standard : STANDARD_BACKUP, + ) + Utility.analytics( 'Advanced Filtering', - `ID: ${advancedFilter.id} Size: ${filterValues.size}`, - type, + `ID: ${id} Size: ${filters.size}`, + category, ) - const handleChange = (event, values) => { - if (values) { - if (event === 'default') { - setFilterValues({ ...values, enabled: filterValues.enabled }) - } else { - setFilterValues({ ...filterValues, [event]: values }) - } - } else { - const { name, value } = event.target - setFilterValues({ - ...filterValues, - [name]: - Array.isArray(value) && type === 'pokestops' - ? value.filter(Boolean).join(',') - : value, - }) - } - } - const footerOptions = [ - { - name: 'reset', - action: () => - handleChange( - 'default', - advancedFilter.standard || { enabled: false, size: 'md' }, - ), - color: 'primary', - size: type === 'pokemon' ? 2 : null, - }, - { - name: 'save', - action: toggleAdvMenu(false, advancedFilter.id, filterValues), - color: 'secondary', - size: type === 'pokemon' ? 3 : null, - }, - ] + /** + * @template {keyof typeof filters} T + * @param {T} key + * @param {(typeof filters)[T]} values + * @returns + */ + const handleChange = (key, values) => + setFilters((prev) => ({ ...prev, [key]: values, all: false })) - if (type === 'pokemon') { - footerOptions.unshift({ - key: 'size', - component: ( - - ), - size: 7, - }) - } - - // Provides a reset if that condition is no longer available - useEffect(() => { - if (type === 'pokestops' && ui.pokestops?.quests) { - if (!questConditions[advancedFilter.id] && filterValues.adv) { - setFilterValues({ ...filterValues, adv: '' }) - } else { - const filtered = questConditions[advancedFilter.id] - ? filterValues.adv - .split(',') - .filter((each) => - questConditions[advancedFilter.id].find( - ({ title }) => title === each, + const toggleClose = + (save = false) => + () => { + setOpen(!open) + if (!save) { + setFilters({ ...standard }) + } else if (id === 'global' && selectedIds?.length) { + const keys = new Set(selectedIds) + useStore.setState((prev) => ({ + filters: { + ...prev.filters, + [category]: { + ...prev.filters[category], + filter: Object.fromEntries( + Object.entries(prev.filters[category].filter).map( + ([key, oldFilter]) => [ + key, + keys.has(key) + ? { + ...filters, + enabled: true, + all: prev.filters[category].easyMode, + } + : oldFilter, + ], ), - ) - : [] - setFilterValues({ - ...filterValues, - adv: filtered.length ? filtered.join(',') : '', - }) + ), + }, + }, + })) } } - }, []) - return advancedFilter.id ? ( - <> + const footerOptions = + /** @type {import('@components/layout/general/Footer').FooterButton[]} */ ( + React.useMemo( + () => [ + { + name: 'reset', + action: () => setFilters({ ...standard }), + color: 'primary', + size: category === 'pokemon' ? 2 : null, + }, + { + name: 'save', + action: () => toggleClose(true)(), + color: 'secondary', + size: category === 'pokemon' ? 3 : null, + }, + ], + [category, filters, id], + ) + ) + + if (!id) return null + return ( +

- - {type === 'pokemon' ? ( - - {userSettings[type].legacyFilter && ui[type].legacy ? ( - - - + + + {category === 'pokemon' ? ( + legacyFilter && 'legacy' in ui ? ( + ) : ( <> - {Object.entries(ui[type].sliders).map(([category, sliders]) => ( - - {sliders.map((each) => ( - - ))} - - ))} - - - setFilterValues({ - ...filterValues, - gender: newValue, - }) - } - /> - - {['xxs', 'xxl'].map((each, i) => ( - - - - {t(i ? 'size_5' : 'size_1')} - - - - { - setFilterValues({ - ...filterValues, - [each]: !filterValues[each], - }) - }} - /> - - - ))} - - + {Object.entries('sliders' in ui ? ui.sliders : {}).map( + ([subCat, sliders], i) => ( + + {sliders.map((each) => ( + + + + ))} + + ), + )} + + + + + + + + + + + - )} - - ) : ( - + ) + ) : ( - - )} - {type === 'pokestops' && - ui.pokestops?.quests && - questConditions?.[advancedFilter.id] && ( - - - {t('quest_condition')} - - - )} + {category === 'pokestops' && } +
- - ) : null +
+ ) } diff --git a/src/components/layout/dialogs/filters/Gender.jsx b/src/components/layout/dialogs/filters/Gender.jsx index 2a108d71c..3ec37838d 100644 --- a/src/components/layout/dialogs/filters/Gender.jsx +++ b/src/components/layout/dialogs/filters/Gender.jsx @@ -1,34 +1,27 @@ +// @ts-check import * as React from 'react' -import { Grid, Typography } from '@mui/material' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' import { useTranslation } from 'react-i18next' -import MultiSelector from '@components/layout/drawer/MultiSelector' +import { MultiSelector } from '@components/layout/drawer/MultiSelector' +import { GENDERS } from '@assets/constants' -export default function GenderFilter({ category, filter, setFilter }) { +/** + * + * @param {{ field: string } & import('@mui/material').ListItemProps} props + * @returns + */ +export function GenderListItem({ field, ...props }) { const { t } = useTranslation() - return ( - <> - {category && ( - - {t('gender')} - - )} - - - - + + {t('gender')} + + ) } diff --git a/src/components/layout/dialogs/filters/QuestConditions.jsx b/src/components/layout/dialogs/filters/QuestConditions.jsx new file mode 100644 index 000000000..cde1b42b1 --- /dev/null +++ b/src/components/layout/dialogs/filters/QuestConditions.jsx @@ -0,0 +1,107 @@ +// @ts-check +import * as React from 'react' +import ListItem from '@mui/material/ListItem' +import { useTranslation } from 'react-i18next' +import { useStatic, useStore } from '@hooks/useStore' +import { setDeep } from '@services/functions/setDeep' +import { FormControl, InputLabel, Select, MenuItem } from '@mui/material' +import Typography from '@mui/material/Typography' +import dlv from 'dlv' +import QuestTitle from '@components/layout/general/QuestTitle' + +/** + * + * @param {{ id: string }} props + * @returns + */ +export function QuestConditionSelector({ id }) { + const value = /** @type {string} */ ( + useStore((s) => dlv(s, `filters.pokestops.filter.${id}.adv`) || '') + ) + const questConditions = useStatic((s) => s.available.questConditions[id]) + const hasQuests = useStatic((s) => s.ui.pokestops?.quests) + + const { t } = useTranslation() + + // Provides a reset if that condition is no longer available + React.useEffect(() => { + if (hasQuests) { + if (!questConditions && value) { + useStore.setState((prev) => + setDeep(prev, `filters.pokestops.filter.${id}.adv`, ''), + ) + } else { + const filtered = questConditions + ? value + .split(',') + .filter((each) => + questConditions.find(({ title }) => title === each), + ) + : [] + useStore.setState((prev) => + setDeep( + prev, + `filters.pokestops.filter.${id}.adv`, + filtered.length ? filtered.join(',') : '', + ), + ) + } + } + }, [questConditions, id, hasQuests]) + + if (!questConditions) return null + + return ( + + + {t('quest_condition')} + + + + ) +} diff --git a/src/components/layout/dialogs/filters/Size.jsx b/src/components/layout/dialogs/filters/Size.jsx index d3bf2ebe6..7ce4ac3a1 100644 --- a/src/components/layout/dialogs/filters/Size.jsx +++ b/src/components/layout/dialogs/filters/Size.jsx @@ -1,30 +1,22 @@ -import React from 'react' -import { ButtonGroup, Button } from '@mui/material' +// @ts-check +import * as React from 'react' +import ListItem from '@mui/material/ListItem' +import ListItemText from '@mui/material/ListItemText' import { useTranslation } from 'react-i18next' -export default function Size({ filterValues, handleChange, btnSize }) { - const sizes = ['sm', 'md', 'lg', 'xl'] +import { MultiSelector } from '@components/layout/drawer/MultiSelector' +import { ICON_SIZES } from '@assets/constants' + +/** + * @param {{ field: string } & import('@mui/material/ListItem').ListItemProps} props + */ +export default function Size({ field, ...props }) { const { t } = useTranslation() return ( - - {sizes.map((size) => { - const color = - (filterValues.size || 'md') === size ? 'primary' : 'secondary' - return ( - - ) - })} - + + {t('icon_size')} + + ) } diff --git a/src/components/layout/dialogs/filters/SliderTile.jsx b/src/components/layout/dialogs/filters/SliderTile.jsx index 393f3b90b..7b956c855 100644 --- a/src/components/layout/dialogs/filters/SliderTile.jsx +++ b/src/components/layout/dialogs/filters/SliderTile.jsx @@ -1,8 +1,33 @@ +// @ts-check /* eslint-disable react/jsx-no-duplicate-props */ -import React, { useState, useEffect } from 'react' -import { Grid, Typography, Slider, TextField } from '@mui/material' +import * as React from 'react' +import Grid2 from '@mui/material/Unstable_Grid2/Grid2' +import TextField from '@mui/material/TextField' +import Slider from '@mui/material/Slider' +import { styled } from '@mui/material/styles' import { useTranslation } from 'react-i18next' +import { ToggleTypography } from '@components/layout/general/ToggleTypography' +import { SLIDER_LABELS } from '@assets/constants' +const StyledTextField = + /** @type {React.FC} */ ( + styled(TextField, { shouldForwardProp: (prop) => prop !== 'textColor' })( + // @ts-ignore + ({ textColor }) => ({ + width: 80, + color: textColor, + }), + ) + ) +const StyledSlider = styled(Slider)(() => ({ width: '100%' })) + +/** + * @param {{ + * filterSlide: import('@rm/types').RMSliderProps, + * handleChange: import('@rm/types').RMSliderHandleChange, + * filterValues: number[] + * }} props + */ export default function SliderTile({ filterSlide: { name, @@ -22,127 +47,125 @@ export default function SliderTile({ handleChange, filterValues, }) { - const values = disabled ? [min, max] : filterValues[name] const { t } = useTranslation() - const [tempValues, setTempValues] = useState(values) - const [tempTextValues, setTempTextValues] = useState(values) - const [fullName, setFullName] = useState(true) - - useEffect(() => { - setTempValues(values) - setTempTextValues(values) - }, [filterValues]) + const [temp, setTemp] = React.useState(filterValues || []) + const [text, setText] = React.useState(filterValues || []) - const handleTempChange = (event, newValues) => { - if (newValues) { - setTempTextValues(newValues) - setTempValues(newValues) - } else { - const { id, value } = event.target - let safeVal = parseInt(value) - if (safeVal === undefined || Number.isNaN(safeVal)) { - safeVal = '' - } - const arrValues = [] - if (id === 'min') { - safeVal = safeVal < min ? min : safeVal - arrValues.push(safeVal, values[1]) - } else { - safeVal = safeVal > max ? max : safeVal - arrValues.push(values[0], safeVal) - } - if (safeVal === '') { - setTempTextValues(arrValues) - } else { - setTempTextValues(arrValues) - handleChange(name, arrValues, low, high) - } - } - } - - if (!tempValues) return null + // console.log({ name, filterValues, min, max }) + const handleSliderChange = + /** @type {import('@mui/material').SliderProps['onChangeCommitted']} */ ( + React.useCallback( + (e, newValues) => { + if (Array.isArray(newValues)) { + if (e.type === 'mousemove') { + setText(newValues) + setTemp(newValues) + } else if (e.type === 'mouseup') { + handleChange(name, newValues, low, high) + } + } + }, + [name, low, high, handleChange], + ) + ) + const handleTextInputChange = + /** @type {import('@mui/material').TextFieldProps['onChange']} */ ( + React.useCallback( + (event) => { + const safeVal = +event.target.value || '' + const arrValues = /** @type {number[]} */ ([]) + if (typeof safeVal === 'number') { + if (event.target.name === 'min') { + arrValues.push(safeVal < min ? min : safeVal, text[1]) + } else { + arrValues.push(text[0], safeVal > max ? max : safeVal) + } + } + if (safeVal === '') { + setText(arrValues) + } else { + setText(arrValues) + handleChange(event.target.name, arrValues, low, high) + } + }, + [text, name, min, max, handleChange, low, high], + ) + ) - const textColor = - (tempValues && tempValues[0] === min && tempValues[1] === max) || disabled - ? 'text.disabled' - : 'inherit' + const colorSx = React.useMemo( + () => ({ + sx: { + color: + (temp && temp[0] === min && temp[1] === max) || disabled + ? 'text.disabled' + : 'inherit', + }, + }), + [temp, min, max, disabled], + ) + const inputProps = React.useMemo( + () => ({ min, max, autoFocus: false }), + [min, max], + ) + const marksMemo = React.useMemo( + () => marks?.map((value) => ({ value, label: t(`${markI18n}${value}`) })), + [marks, markI18n], + ) - const translated = t(i18nKey || `slider_${name}`) + React.useEffect(() => { + const values = disabled || !filterValues ? [min, max] : filterValues + if (values.some((v, i) => v !== temp[i])) setTemp(values) + if (values.some((v, i) => v !== text[i])) setText(values) + }, [filterValues?.[0], filterValues?.[1], disabled, min, max]) + if (!temp || !text) return null return ( - - - setFullName(!fullName)} - color={textColor} - > - {translated} - - - {(noTextInput ? [] : ['min', 'max']).map((each, index) => ( - - - - ))} - - + + {t(i18nKey || `slider_${name}`)} + + + {!noTextInput && + SLIDER_LABELS.map((each, index) => ( + + + + ))} + + { - handleChange(name, newValues, low, high) - }} + value={temp} + onChange={handleSliderChange} + onChangeCommitted={handleSliderChange} disabled={disabled} valueLabelFormat={marks ? (e) => t(`${markI18n}${e}`) : undefined} step={step} - marks={ - marks - ? marks.map((each) => ({ - value: each, - label: t(`${markI18n}${each}`), - })) - : undefined - } + marks={marksMemo} /> - - + + ) } diff --git a/src/components/layout/dialogs/filters/StringFilter.jsx b/src/components/layout/dialogs/filters/StringFilter.jsx index a67192c3e..1bfe67b02 100644 --- a/src/components/layout/dialogs/filters/StringFilter.jsx +++ b/src/components/layout/dialogs/filters/StringFilter.jsx @@ -1,58 +1,73 @@ -import React, { useState } from 'react' -import { TextField } from '@mui/material' +// @ts-check +import * as React from 'react' +import { ListItem, TextField } from '@mui/material' import { useTranslation } from 'react-i18next' +import dlv from 'dlv' +import { useStore } from '@hooks/useStore' +import { setDeep } from '@services/functions/setDeep' import Utility from '@services/Utility' -export default function StringFilter({ filterValues, setFilterValues }) { +/** + * Expert string input field for filters + * @param {{ field: string } & import('@mui/material').ListItemProps} props + * @returns + */ +export function StringFilter({ field, ...props }) { const { t } = useTranslation() - const [validation, setValidation] = useState({ - value: filterValues.adv, + const value = useStore((s) => dlv(s, `${field}.adv`)) + + const [validation, setValidation] = React.useState({ status: false, label: t('iv_or_filter'), message: t('overwrites'), }) - const validationCheck = (event) => { - let { value } = event.target - Utility.analytics('Filtering', value, 'Legacy') - if (Utility.checkAdvFilter(value)) { - setValidation({ - label: t('valid'), - value, - status: false, - message: t('valid_filter'), - }) - } else if (value === '') { - setValidation({ - label: t('iv_or_filter'), - value, - status: false, - message: t('overwrites'), - }) - } else { - setValidation({ - label: t('invalid'), - value, - status: true, - message: t('invalid_filter'), - }) - value = '' - } - setFilterValues({ ...filterValues, adv: value }) - } + const validationCheck = + /** @type {import('@mui/material').TextFieldProps['onChange']} */ ( + React.useCallback( + (event) => { + const newValue = event.target.value + Utility.analytics('Filtering', newValue, 'Legacy') + if (Utility.checkAdvFilter(newValue)) { + setValidation({ + label: t('valid'), + status: false, + message: t('valid_filter'), + }) + } else if (newValue === '') { + setValidation({ + label: t('iv_or_filter'), + status: false, + message: t('overwrites'), + }) + } else { + setValidation({ + label: t('invalid'), + status: true, + message: t('invalid_filter'), + }) + } + useStore.setState((prev) => setDeep(prev, `${field}.adv`, newValue)) + }, + [field], + ) + ) return ( - + + + ) } + +export const StringFilterMemo = React.memo(StringFilter) diff --git a/src/components/layout/dialogs/tutorial/Sliders.jsx b/src/components/layout/dialogs/tutorial/Sliders.jsx index d9ddc3ef0..56a8a53e5 100644 --- a/src/components/layout/dialogs/tutorial/Sliders.jsx +++ b/src/components/layout/dialogs/tutorial/Sliders.jsx @@ -64,7 +64,7 @@ export default function TutSliders() { ))} diff --git a/src/components/layout/dialogs/webhooks/WebhookAdv.jsx b/src/components/layout/dialogs/webhooks/WebhookAdv.jsx index 24f525745..eb8ccf866 100644 --- a/src/components/layout/dialogs/webhooks/WebhookAdv.jsx +++ b/src/components/layout/dialogs/webhooks/WebhookAdv.jsx @@ -529,7 +529,7 @@ export default function WebhookAdvanced({ )) diff --git a/src/components/layout/drawer/Areas.jsx b/src/components/layout/drawer/Areas.jsx index e1fab25c8..2ee30d7ac 100644 --- a/src/components/layout/drawer/Areas.jsx +++ b/src/components/layout/drawer/Areas.jsx @@ -23,7 +23,7 @@ import { useStatic, useStore } from '@hooks/useStore' import AreaTile from './AreaTile' import { ItemSearch } from './ItemSearch' -export default function AreaDropDown() { +function AreaDropDown() { const { data, loading, error } = useQuery(Query.scanAreasMenu()) const { t } = useTranslation() const filters = useStore((s) => s.filters) @@ -167,3 +167,5 @@ export default function AreaDropDown() { ) } + +export default React.memo(AreaDropDown) diff --git a/src/components/layout/drawer/BoolToggle.jsx b/src/components/layout/drawer/BoolToggle.jsx index 13abfc452..54447df6d 100644 --- a/src/components/layout/drawer/BoolToggle.jsx +++ b/src/components/layout/drawer/BoolToggle.jsx @@ -3,54 +3,72 @@ import * as React from 'react' import ListItem from '@mui/material/ListItem' import ListItemText from '@mui/material/ListItemText' import Switch from '@mui/material/Switch' +import List from '@mui/material/List' -import { useStore } from '@hooks/useStore' +import { useDeepStore } from '@hooks/useStore' import { useTranslation } from 'react-i18next' +import Grid2 from '@mui/material/Unstable_Grid2/Grid2' +import Utility from '@services/Utility' import { fromSnakeCase } from '@services/functions/fromSnakeCase' -import dlv from 'dlv' -import { setDeep } from '@services/functions/setDeep' /** - * @param {{ - * field: string, + * @typedef {{ + * field: import('@hooks/useStore').UseStorePaths, * label?: string, * disabled?: boolean, * children?: React.ReactNode, - * onChange?: import('@mui/material/Switch').SwitchProps['onChange'], - * }} props - * @returns {JSX.Element} + * align?: import('@mui/material').TypographyProps['align'] + * } & import('@mui/material').ListItemProps} BoolToggleProps */ -export default function BoolToggle({ + +/** @param {BoolToggleProps} props */ +export function BoolBase({ field, - label, + label = field.split('.').at(-1), disabled = false, children, - onChange, + align, + ...props }) { - const value = useStore((s) => dlv(s, field)) const { t } = useTranslation() - - const onChangeWrapper = React.useCallback( - ( - /** @type {React.ChangeEvent} */ _, - /** @type {boolean} */ checked, - ) => { - useStore.setState((prev) => setDeep(prev, field, checked)) - if (onChange) onChange(_, checked) - }, - [field], - ) + const [value, setValue] = useDeepStore(field, false) + const onChange = + /** @type {import('@mui/material').SwitchProps['onChange']} */ ( + React.useCallback((_, checked) => setValue(checked), [field]) + ) return ( - + {children} - - {t(label, fromSnakeCase(label)) ?? t(field, fromSnakeCase(field))} + + {t(label, t(Utility.camelToSnake(label), fromSnakeCase(label)))} - + ) } + +export const BoolToggle = React.memo( + BoolBase, + (prev, next) => prev.disabled === next.disabled && prev.field === next.field, +) + +/** @param {{ items: readonly [string, string] } & BoolToggleProps} props */ +export function DualBoolToggle({ items, field, ...props }) { + return ( + + {items.map((item) => ( + + + + ))} + + ) +} diff --git a/src/components/layout/drawer/CollapsibleItem.jsx b/src/components/layout/drawer/CollapsibleItem.jsx index 1470a6193..1678c33e7 100644 --- a/src/components/layout/drawer/CollapsibleItem.jsx +++ b/src/components/layout/drawer/CollapsibleItem.jsx @@ -1,10 +1,12 @@ -/* eslint-disable no-unused-vars */ +// @ts-check import * as React from 'react' import Collapse from '@mui/material/Collapse' -import ListItem from '@mui/material/ListItem' import List from '@mui/material/List' -export default function CollapsibleItem({ open, children }) { +/** + * @param {{ open: boolean, children: React.ReactNode }} props + */ +export function CollapsibleItem({ open, children }) { return ( diff --git a/src/components/layout/drawer/Extras.jsx b/src/components/layout/drawer/Extras.jsx index 7e857b6b3..5f51a0a89 100644 --- a/src/components/layout/drawer/Extras.jsx +++ b/src/components/layout/drawer/Extras.jsx @@ -1,3 +1,6 @@ +/* eslint-disable no-fallthrough */ +/* eslint-disable default-case */ +// @ts-check import * as React from 'react' import { Switch, @@ -9,342 +12,346 @@ import { } from '@mui/material' import { Trans, useTranslation } from 'react-i18next' -import { useStatic, useStore } from '@hooks/useStore' +import { useDeepStore, useStatic, useStore } from '@hooks/useStore' +import { + BADGES, + FORT_LEVELS, + QUEST_SETS, + S2_LEVELS, + SPAWNPOINT_TTH, + WAYFARER_OPTIONS, +} from '@assets/constants' -import MultiSelector from './MultiSelector' +import { MultiSelector } from './MultiSelector' import SliderTile from '../dialogs/filters/SliderTile' -import CollapsibleItem from './CollapsibleItem' +import { CollapsibleItem } from './CollapsibleItem' -export default function Extras({ category, subItem, data }) { - const { t } = useTranslation() - const available = useStatic((s) => s.available) - const Icons = useStatic((s) => s.Icons) - const filters = useStore((s) => s.filters) - const { setFilters } = useStore.getState() - const { - config: { - misc: { enableConfirmedInvasions, enableQuestSetSelector }, - }, - filters: staticFilters, - } = useStatic.getState() +const BaseNestSlider = () => { + const slider = useStatic((s) => s.ui.nests?.sliders?.secondary?.[0]) + const [filters, setFilters] = useDeepStore(`filters.nests.avgFilter`) + if (!filters || !slider) return null + return ( + + setFilters(values)} + filterValues={filters} + /> + + ) +} +const NestSlider = React.memo(BaseNestSlider) - if (category === 'nests' && subItem === 'sliders' && filters[category]) { - return ( +const BaseS2Cells = () => { + const { t } = useTranslation() + const enabled = useStore((s) => !!s.filters.s2cells.enabled) + const [filters, setFilters] = useDeepStore('filters.s2cells.cells') + const safe = React.useMemo(() => filters || [], [filters]) + if (!filters) return null + return ( + - - setFilters({ - ...filters, - [category]: { - ...filters[category], - avgFilter: values, - }, - }) + + + + ) +} +const S2Cells = React.memo(BaseS2Cells) + +/** @param {{ category: 'pokestops' | 'gyms', subItem: string }} props */ +const BaseAllForts = ({ category, subItem }) => { + const { t } = useTranslation() + const enabled = useStore((s) => !!s.filters?.[category]?.[subItem]) + return ( + + + + - ) - } + + ) +} +const AllForts = React.memo(BaseAllForts) - if (category === 's2cells' && subItem === 'cells') { - return ( - - +const BaseGymBadges = () => { + const enabled = useStore((s) => !!s.filters?.gyms?.gymBadges) + return ( + + + + + + ) +} +const GymBadges = React.memo(BaseGymBadges) + +const BaseRaids = () => { + const { t } = useTranslation() + const available = useStatic((s) => s.available.gyms) + const enabled = useStore((s) => !!s.filters?.gyms?.raids) + const [filters, setFilters] = useDeepStore('filters.gyms.raidTier', '') + return ( + + selected.join(', ')} - multiple - onChange={({ target }) => - setFilters({ - ...filters, - [category]: { - ...filters[category], - [subItem]: target.value, - }, - }) + value={filters} + fullWidth + size="small" + onChange={(e) => + setFilters(e.target.value === 'all' ? 'all' : +e.target.value) } > - {[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((level) => ( - - Level {level} + {[ + 'all', + ...available + .filter((x) => x.startsWith('r')) + .map((y) => +y.slice(1)), + ].map((tier, i) => ( + + {t(i ? `raid_${tier}_plural` : 'disabled')} ))} - - - ) - } + } + > + + + + ) +} +const Raids = React.memo(BaseRaids) - if ( - (category === 'pokestops' && subItem === 'allPokestops') || - (category === 'gyms' && subItem === 'allGyms') - ) { - return ( - - - } - > - - - - ) - } +const BaseQuestSet = () => { + const enabled = useStore((s) => !!s.filters?.pokestops?.quests) + return ( + + + + + + ) +} +const QuestSet = React.memo(BaseQuestSet) - if (category === 'gyms') { - if (subItem === 'gymBadges') { - return ( - - - - - - ) - } - if (subItem === 'raids') { - return ( - - { - setFilters({ - ...filters, - [category]: { - ...filters[category], - raidTier: - e.target.value === 'all' ? 'all' : +e.target.value, - }, - }) - }} - > - {[ - 'all', - ...available.gyms - .filter((x) => x.startsWith('r')) - .map((y) => +y.slice(1)), - ].map((tier, i) => ( - - {t(i ? `raid_${tier}_plural` : 'disabled')} - - ))} - - } - > - - - - ) - } - } +const BaseInvasion = () => { + const { t } = useTranslation() + const enabled = useStore((s) => !!s.filters?.pokestops?.invasions) + const [filters, setFilters] = useDeepStore( + 'filters.pokestops.confirmed', + false, + ) + return ( + + + + setFilters((prev) => !prev)} + /> + + + ) +} +const Invasion = React.memo(BaseInvasion) - if (category === 'pokestops') { - if (enableQuestSetSelector === true && subItem === 'quests') { - return ( - - - - - - ) - } - if (enableConfirmedInvasions === true && subItem === 'invasions') { - return ( - - - - { - setFilters({ - ...filters, - [category]: { - ...filters[category], - confirmed: !filters[category].confirmed, - }, - }) - }} - /> - - - ) - } - if (subItem === 'eventStops') { - return ( - - {available?.pokestops - .filter((event) => event.startsWith('b')) - .map((event) => ( - - - {t(`display_type_${event.slice(1)}`)} - - - { - setFilters({ - ...filters, - [category]: { - ...filters[category], - filter: { - ...filters[category].filter, - [event]: { - ...filters[category].filter[event], - enabled: !filters[category].filter[event].enabled, - }, - }, - }, - }) - }} - /> - - ))} - - ) +/** @param {{ id: string }} props */ +const IndividualEvent = ({ id }) => { + const { t } = useTranslation() + const Icons = useStatic((s) => s.Icons) + const [filters, setFilters] = useDeepStore( + `filters.pokestops.filter.${id}.enabled`, + false, + ) + return ( + + + {t(`display_type_${id.slice(1)}`)} + + + setFilters((prev) => !prev)} /> + + ) +} +const BaseEventStops = () => { + const available = useStatic((s) => s.available.pokestops) + const enabled = useStore((s) => !!s.filters?.pokestops?.eventStops) + return ( + + {available + ?.filter((event) => event.startsWith('b')) + .map((event) => ( + + ))} + + ) +} +const EventStops = React.memo(BaseEventStops) + +/** @param {{ item: (typeof WAYFARER_OPTIONS)[number], index: number, disabled: boolean }} props */ +const WayfarerOption = ({ item, index, disabled }) => { + const { t } = useTranslation() + const [filters, setFilters] = useDeepStore( + `filters.submissionCells.${item}`, + false, + ) + return ( + + 1 ? ( + + {{ level: item.substring(1, 3) }} + + ) : ( + t(index ? 'include_sponsored' : 'poi') + ) + } + /> + setFilters((prev) => !prev)} + disabled={disabled} + /> + + ) +} +const SubmissionCells = () => { + const enabled = useStore((s) => !!s.filters?.submissionCells?.enabled) + return ( + + {WAYFARER_OPTIONS.map((item, i) => ( + + ))} + + ) +} +const BaseSubmissionCells = React.memo(SubmissionCells) + +const BaseRouteSlider = () => { + const enabled = useStore((s) => !!s.filters?.routes?.enabled) + const [filters, setFilters] = useDeepStore('filters.routes.distance') + const baseDistance = useStatic.getState().filters?.routes?.distance + + /** @type {import('@rm/types').RMSliderProps} */ + const slider = React.useMemo(() => { + const min = baseDistance?.[0] || 0 + const max = baseDistance?.[1] || 25 + return { + color: 'secondary', + disabled: false, + min, + max, + i18nKey: 'distance', + step: 0.5, + name: 'distance', + label: 'km', } - } + }, [baseDistance]) - if ( - category === 'wayfarer' && - subItem === 'submissionCells' && - filters[subItem] - ) { - return ( - - {['rings', 'includeSponsored', 's14Cells', 's17Cells'].map( - (item, i) => ( - - 1 ? ( - - {{ level: item.substring(1, 3) }} - - ) : ( - t(i ? 'include_sponsored' : 'poi') - ) - } - /> - { - setFilters({ - ...filters, - [subItem]: { - ...filters[subItem], - [item]: !filters[subItem][item], - }, - }) - }} - disabled={filters[subItem]?.enabled === false} - /> - - ), - )} - - ) - } - if (category === 'routes' && subItem === 'enabled' && staticFilters.routes) { - return ( - - - - setFilters({ - ...filters, - [category]: { - ...filters[category], - distance: values, - }, - }) - } - filterValues={filters[category]} - /> - - - ) - } + return ( + + + setFilters(values)} + filterValues={filters} + /> + + + ) +} +const RouteSlider = React.memo(BaseRouteSlider) - if (category === 'admin' && subItem === 'spawnpoints') { - return ( - - - - - - ) +const BaseSpawnpointTTH = () => { + const enabled = useStore((s) => !!s.filters?.spawnpoints?.enabled) + return ( + + + + + + ) +} +const SpawnpointTTH = React.memo(BaseSpawnpointTTH) + +function Extras({ category, subItem }) { + const { enableConfirmedInvasions, enableQuestSetSelector } = + useStatic.getState().config.misc + + switch (category) { + case 'nests': + return subItem === 'sliders' ? : null + case 's2cells': + return subItem === 'cells' ? : null + case 'pokestops': + switch (subItem) { + case 'allPokestops': + return + case 'quests': + return enableQuestSetSelector ? : null + case 'invasions': + return enableConfirmedInvasions ? : null + case 'eventStops': + return + } + case 'gyms': + switch (subItem) { + case 'allGyms': + return + case 'gymBadges': + return + case 'raids': + return + } + case 'wayfarer': + return subItem === 'submissionCells' ? : null + case 'routes': + return subItem === 'enabled' ? : null + case 'admin': + return subItem === 'spawnpoints' ? : null + default: + return null } - return null } + +export default React.memo( + Extras, + (prev, next) => + prev.category === next.category && prev.subItem === next.subItem, +) diff --git a/src/components/layout/drawer/ItemSearch.jsx b/src/components/layout/drawer/ItemSearch.jsx index 30651cfdb..b9c6f3f04 100644 --- a/src/components/layout/drawer/ItemSearch.jsx +++ b/src/components/layout/drawer/ItemSearch.jsx @@ -1,14 +1,12 @@ // @ts-check import * as React from 'react' +import { useTranslation } from 'react-i18next' import ListItem from '@mui/material/ListItem' import TextField from '@mui/material/TextField' import IconButton from '@mui/material/IconButton' import HighlightOffIcon from '@mui/icons-material/HighlightOff' -import { useTranslation } from 'react-i18next' -import dlv from 'dlv' -import { useStore } from '@hooks/useStore' -import { setDeep } from '@services/functions/setDeep' +import { useDeepStore } from '@hooks/useStore' /** * @param {{ @@ -20,7 +18,24 @@ import { setDeep } from '@services/functions/setDeep' */ export function ItemSearch({ field, label = 'search', disabled }) { const { t } = useTranslation() - const value = useStore((s) => dlv(s, field)) + const [value, setValue] = useDeepStore(field, '') + + const InputProps = React.useMemo( + () => ({ + endAdornment: ( + setValue('')}> + + + ), + }), + [value, setValue], + ) + + /** @type {import('@mui/material').TextFieldProps['onChange']} */ + const onChange = React.useCallback( + (e) => setValue(e.target.value || ''), + [setValue], + ) return ( @@ -30,26 +45,18 @@ export function ItemSearch({ field, label = 'search', disabled }) { fullWidth size="small" disabled={disabled} - value={value || ''} - onChange={(e) => - useStore.setState((prev) => - setDeep(prev, field, e.target.value || ''), - ) - } - InputProps={{ - endAdornment: ( - - useStore.setState((prev) => setDeep(prev, field, '')) - } - > - - - ), - }} + value={value} + onChange={onChange} + InputProps={InputProps} /> ) } + +export const ItemSearchMemo = React.memo( + ItemSearch, + (prev, next) => + prev.disabled === next.disabled && + prev.field === next.field && + prev.label === next.label, +) diff --git a/src/components/layout/drawer/MultiSelector.jsx b/src/components/layout/drawer/MultiSelector.jsx index 5537aac2b..d7bd0d968 100644 --- a/src/components/layout/drawer/MultiSelector.jsx +++ b/src/components/layout/drawer/MultiSelector.jsx @@ -1,21 +1,24 @@ -import React from 'react' +// @ts-check +import * as React from 'react' import { ButtonGroup, Button } from '@mui/material' import { useTranslation } from 'react-i18next' +import { useStore } from '@hooks/useStore' +import dlv from 'dlv' +import { setDeep } from '@services/functions/setDeep' -export default function MultiSelector({ - filters, - setFilters, - category, - filterKey, - items, - tKey, - allowNone, -}) { +/** + * + * @param {{ + * field: string, + * items: readonly (string | number)[], + * tKey?: string, + * allowNone?: boolean, + * }} props + * @returns + */ +export function MultiSelector({ field, items, tKey, allowNone = false }) { const { t } = useTranslation() - const filterValue = - typeof filters === 'object' && category - ? filters[category]?.[filterKey] - : filters + const value = useStore((s) => dlv(s, field)) return ( @@ -23,23 +26,14 @@ export default function MultiSelector({ ))} diff --git a/src/components/layout/drawer/Pokemon.jsx b/src/components/layout/drawer/Pokemon.jsx index c6fc94390..2272c4200 100644 --- a/src/components/layout/drawer/Pokemon.jsx +++ b/src/components/layout/drawer/Pokemon.jsx @@ -1,9 +1,8 @@ +// @ts-check /* eslint-disable react/no-unstable-nested-components */ -import React, { useEffect, useState, Fragment } from 'react' +import * as React from 'react' import { - Grid, Typography, - Switch, AppBar, Tab, Tabs, @@ -12,7 +11,6 @@ import { ListItemText, Box, IconButton, - Dialog, ButtonGroup, Collapse, Tooltip, @@ -30,29 +28,26 @@ import TuneIcon from '@mui/icons-material/Tune' import CheckIcon from '@mui/icons-material/Check' import ClearIcon from '@mui/icons-material/Clear' -import { useStatic, useStore } from '@hooks/useStore' +import { useDeepStore, useStatic, useStore } from '@hooks/useStore' import Utility from '@services/Utility' -import StringFilter from '../dialogs/filters/StringFilter' +import { FILTER_SIZES, IV_OVERRIDES } from '@assets/constants' + +import { StringFilterMemo } from '../dialogs/filters/StringFilter' import SliderTile from '../dialogs/filters/SliderTile' import TabPanel from '../general/TabPanel' -import MultiSelector from './MultiSelector' import AdvancedFilter from '../dialogs/filters/Advanced' -import BoolToggle from './BoolToggle' -import { ItemSearch } from './ItemSearch' +import { BoolToggle, DualBoolToggle } from './BoolToggle' +import { ItemSearchMemo } from './ItemSearch' +import { GenderListItem } from '../dialogs/filters/Gender' function AvailableSelector() { const available = useStatic((s) => s.available.pokemon) const { t } = useTranslation() const Icons = useStatic((s) => s.Icons) const filters = useStore((s) => s.filters.pokemon) - const isMobile = useStatic((s) => s.isMobile) const search = useStore((s) => s.searches.pokemonQuickSelect || '') - const [advanced, setAdvanced] = React.useState({ - id: '0', - standard: filters.standard, - tempFilters: filters.standard, - }) + const [advanced, setAdvanced] = React.useState('global') const [open, setOpen] = React.useState(false) const items = React.useMemo(() => { @@ -92,51 +87,7 @@ function AvailableSelector() { search, ]) - const onClose = (e, id, filter) => () => { - if (e !== undefined) { - const keys = new Set(items.map((item) => item.key)) - useStore.setState((prev) => ({ - filters: { - ...prev.filters, - pokemon: { - ...prev.filters.pokemon, - filter: - id === 'global' - ? Object.fromEntries( - Object.entries(prev.filters.pokemon.filter).map( - ([key, oldFilter]) => [ - key, - keys.has(key) - ? { - ...filter, - enabled: true, - all: prev.filters.pokemon.easyMode, - } - : oldFilter, - ], - ), - ) - : { - ...prev.filters.pokemon.filter, - [id]: { - ...filter, - enabled: true, - all: prev.filters.pokemon.easyMode, - }, - }, - }, - }, - })) - } - setAdvanced(filters.standard) - setOpen(false) - if (id === 'global') setAll('advanced') - } - - /** - * - * @param {'enable' | 'disable' | 'advanced'} action - */ + /** @param {'enable' | 'disable' | 'advanced'} action */ const setAll = (action) => { const keys = new Set(items.map((item) => item.key)) useStore.setState((prev) => ({ @@ -158,7 +109,7 @@ function AvailableSelector() { return ( - + { - setAdvanced((prev) => ({ - ...prev, - id: 'global', - tempFilters: filters.filter.global, - })) + setAdvanced('global') setOpen(true) }} > @@ -212,8 +159,7 @@ function AvailableSelector() { justifyContent="center" alignItems="center" position="relative" - outline="ButtonText 1px solid" - sx={{ aspectRatio: '1/1' }} + sx={{ aspectRatio: '1/1', outline: 'ButtonText 1px solid' }} onClick={() => { const filter = { ...filters.filter[item.key] } if (filter.all) { @@ -265,11 +211,7 @@ function AvailableSelector() { sx={{ position: 'absolute', right: 0, top: 0 }} onClick={(e) => { e.stopPropagation() - setAdvanced((prev) => ({ - ...prev, - id: item.key, - tempFilters: filters.filter[item.key], - })) + setAdvanced(item.key) setOpen(true) }} > @@ -281,14 +223,12 @@ function AvailableSelector() { }} /> {!filters.easyMode && ( - - - + )} ) @@ -298,49 +238,24 @@ const MemoAvailableSelector = React.memo(AvailableSelector, () => true) export default function WithSliders({ category, context }) { const userSettings = useStore((s) => s.userSettings[category]) - const filters = useStore((s) => s.filters) const filterMode = useStore((s) => s.getPokemonFilterMode()) - const { setFilters } = useStore.getState() - + const [ivOr, setIvOr] = useDeepStore('filters.pokemon.ivOr') const { t } = useTranslation() - const [tempLegacy, setTempLegacy] = useState(filters[category].ivOr) - const [openTab, setOpenTab] = useState(0) - const selectRef = React.useRef(/** @type {HTMLDivElement | null} */ (null)) + const [openTab, setOpenTab] = React.useState(0) - useEffect(() => { - setFilters({ - ...filters, - [category]: { - ...filters[category], - ivOr: tempLegacy, - }, - }) - }, [tempLegacy]) + const selectRef = React.useRef(/** @type {HTMLDivElement | null} */ (null)) - const handleChange = (event, values) => { - if (values) { - setTempLegacy({ - ...tempLegacy, - [event]: values, - }) - Utility.analytics( - 'Global Pokemon', - `${event}: ${values}`, - `${category} Text`, - ) - } else { - const { name, value } = event.target - setTempLegacy({ - ...tempLegacy, - [name]: value, - }) - Utility.analytics( - 'Global Pokemon', - `${name}: ${value}`, - `${category} Sliders`, - ) + /** @type {import('@rm/types').RMSliderHandleChange} */ + const handleChange = React.useCallback((name, values) => { + if (name in ivOr) { + setIvOr(name, values) } - } + Utility.analytics( + 'Global Pokemon', + `${name}: ${values}`, + `${category} Text`, + ) + }, []) const handleTabChange = (_e, newValue) => setOpenTab(newValue) @@ -408,12 +323,7 @@ export default function WithSliders({ category, context }) { /> {userSettings.legacyFilter && context.legacy ? ( - - - + ) : ( <> @@ -431,98 +341,30 @@ export default function WithSliders({ category, context }) { ))} {index ? ( - - - {['xxs', 'xxl'].map((each, i) => ( - - - - {t(i ? 'size_5' : 'size_1')} - - - - { - setFilters({ - ...filters, - [category]: { - ...filters[category], - ivOr: { - ...filters[category].ivOr, - [each]: !filters[category].ivOr[each], - }, - }, - }) - }} - /> - - - ))} - - + ) : ( <> - - - - setFilters({ - ...filters, - [category]: { - ...filters[category], - ivOr: { - ...filters[category].ivOr, - gender: newValue, - }, - }, - }) - } - /> - + {t('quick_select')} - - - {['zeroIv', 'hundoIv'].map((each) => ( - - - - {t(Utility.camelToSnake(each))} - - - - { - setFilters({ - ...filters, - [category]: { - ...filters[category], - [each]: !filters[category][each], - }, - }) - }} - /> - - - ))} - - + )} diff --git a/src/components/layout/drawer/Section.jsx b/src/components/layout/drawer/Section.jsx index ff9936292..1b340109a 100644 --- a/src/components/layout/drawer/Section.jsx +++ b/src/components/layout/drawer/Section.jsx @@ -38,11 +38,6 @@ export default function DrawerSection({ category, value }) { }> {t(Utility.camelToSnake(category))} @@ -54,10 +49,10 @@ export default function DrawerSection({ category, value }) { ) : category === 'settings' ? ( ) : ( - Object.entries(value).map(([subItem, subValue]) => ( + Object.keys(value).map((subItem) => ( - + )) )} diff --git a/src/components/layout/drawer/Settings.jsx b/src/components/layout/drawer/Settings.jsx index d0e3d4e2d..607ca5b51 100644 --- a/src/components/layout/drawer/Settings.jsx +++ b/src/components/layout/drawer/Settings.jsx @@ -34,7 +34,7 @@ import { } from '@services/desktopNotification' import DrawerActions from './Actions' -import BoolToggle from './BoolToggle' +import { BoolToggle } from './BoolToggle' import LocaleSelection from '../general/LocaleSelection' function FCSelect({ name, label, value, onChange, children, icon }) { @@ -168,7 +168,7 @@ export default function Settings() { - + @@ -212,7 +212,7 @@ export default function Settings() { ))} {process.env.NODE_ENV === 'development' && ( - + diff --git a/src/components/layout/general/Footer.jsx b/src/components/layout/general/Footer.jsx index 89e350373..628bdb469 100644 --- a/src/components/layout/general/Footer.jsx +++ b/src/components/layout/general/Footer.jsx @@ -1,10 +1,35 @@ -/* eslint-disable no-nested-ternary */ -import React from 'react' +// @ts-check +import * as React from 'react' import { IconButton, Button, Typography, Grid } from '@mui/material' import { useTranslation } from 'react-i18next' import * as MuiIcons from './Icons' +/** + * @typedef {{ + * key?: string, + * name?: string, + * icon?: string, + * color?: import('@mui/material').ButtonProps['color'], + * disabled?: boolean, + * link?: string, + * target?: string, + * action?: () => void, + * component?: React.ReactNode, + * size?: number, + * align?: 'left' | 'center' | 'right', + * mobileOnly?: boolean, + * }} FooterButton + */ + +/** + * + * @param {{ + * options: FooterButton[], + * role?: string, + * }} props + * @returns + */ export default function Footer({ options, role }) { const { t } = useTranslation() @@ -34,7 +59,7 @@ export default function Footer({ options, role }) { ) } const MuiIcon = button.icon ? MuiIcons[button.icon] : null - const color = button.disabled ? 'default' : button.color + const color = button.disabled ? 'inherit' : button.color const muiColor = ['primary', 'secondary', 'success', 'error'].includes( color, ) diff --git a/src/components/layout/general/Header.jsx b/src/components/layout/general/Header.jsx index b95652c85..87780b67d 100644 --- a/src/components/layout/general/Header.jsx +++ b/src/components/layout/general/Header.jsx @@ -1,9 +1,19 @@ -import React from 'react' +// @ts-check +import * as React from 'react' import Clear from '@mui/icons-material/Clear' import { IconButton, DialogTitle } from '@mui/material' import { Trans, useTranslation } from 'react-i18next' +/** + * + * @param {{ + * names?: string[], + * titles: string[], + * action?: () => void, + * }} props + * @returns + */ export default function Header({ names = [], titles, action }) { const { t } = useTranslation() diff --git a/src/components/layout/general/Menu.jsx b/src/components/layout/general/Menu.jsx index 959952d90..9a76d71b3 100644 --- a/src/components/layout/general/Menu.jsx +++ b/src/components/layout/general/Menu.jsx @@ -336,10 +336,12 @@ export default function Menu({ fullScreen={isMobile && category === 'pokemon'} > diff --git a/src/components/layout/general/TabPanel.jsx b/src/components/layout/general/TabPanel.jsx index 32db7607d..a6b86c52b 100644 --- a/src/components/layout/general/TabPanel.jsx +++ b/src/components/layout/general/TabPanel.jsx @@ -1,16 +1,38 @@ -import React from 'react' -import { Box } from '@mui/material' +// @ts-check +import * as React from 'react' +import Box from '@mui/material/Box' -export default ({ children, value, index, virtual, disablePadding }) => ( - -) +/** + * + * @param {{ + * children: React.ReactNode, + * value: number, + * index: number, + * virtual?: boolean, + * disablePadding?: boolean, + * }} props + */ +export default function TabPanel({ + children, + value, + index, + virtual, + disablePadding, +}) { + return ( + + ) +} diff --git a/src/components/layout/general/ToggleTypography.jsx b/src/components/layout/general/ToggleTypography.jsx new file mode 100644 index 000000000..45d085e4f --- /dev/null +++ b/src/components/layout/general/ToggleTypography.jsx @@ -0,0 +1,10 @@ +// @ts-check +import * as React from 'react' +import Typography from '@mui/material/Typography' + +/** @param {import('@mui/material').TypographyProps} props */ +export function ToggleTypography(props) { + const [fullName, setFullName] = React.useState(true) + const onClick = React.useCallback(() => setFullName((prev) => !prev), []) + return +} diff --git a/src/hooks/useStore.js b/src/hooks/useStore.js index e3b38733b..4cea70ca7 100644 --- a/src/hooks/useStore.js +++ b/src/hooks/useStore.js @@ -1,4 +1,7 @@ import Utility from '@services/Utility' +import { setDeep } from '@services/functions/setDeep' +import dlv from 'dlv' +import { useMemo } from 'react' import { create } from 'zustand' import { persist, createJSONStorage } from 'zustand/middleware' @@ -23,7 +26,7 @@ import { persist, createJSONStorage } from 'zustand/middleware' * tutorial: boolean, * searchTab: string, * search: string, - * filters: object, + * filters: import('@rm/types').AllFilters, * icons: Record * audio: Record * userSettings: Record @@ -33,6 +36,8 @@ import { persist, createJSONStorage } from 'zustand/middleware' * setPokemonFilterMode: (legacyFilter: boolean, easyMode: boolean) => void, * getPokemonFilterMode: () => 'basic' | 'intermediate' | 'expert', * }} UseStore + * @typedef {import('@rm/types').Paths} UseStorePaths + * * @type {import("zustand").UseBoundStore>} */ export const useStore = create( @@ -143,6 +148,39 @@ export const useStore = create( }, ), ) +// * @template {U extends string ? Value[U] : undefined} V + +/** + * @template {UseStorePaths} Paths + * @template {import('@rm/types').ConfigPathValue} T + * @template {T | ((prevValue: T) => T) | keyof T} U + * @param {Paths} field TODO: Remove `string` in long term + * @param {import('@rm/types').ConfigPathValue} [defaultValue] + * @returns {[import('@rm/types').ConfigPathValue, (arg1: U, ...rest: (U extends keyof T ? [arg2: T[U]] : [arg2?: never])) => void]} + */ +export function useDeepStore(field, defaultValue) { + const value = useStore((s) => dlv(s, field, defaultValue)) + return useMemo( + () => [ + value, + (...args) => { + const first = field.split('.').at(0) + const path = field.split('.').slice(1).join('.') + const key = typeof args[0] === 'string' && args[1] ? `.${args[0]}` : '' + const nextValue = + args.length === 1 + ? typeof args[0] === 'function' + ? args[0](value) + : args[0] + : args[1] + return useStore.setState((prev) => ({ + [first]: setDeep(prev[first], `${path}${key}`, nextValue), + })) + }, + ], + [value], + ) +} /** * TODO: Finish this @@ -155,9 +193,9 @@ export const useStore = create( * Icons: InstanceType, * Audio: InstanceType, * config: import('@rm/types').Config['map'], - * ui: object + * ui: ReturnType * auth: { perms: Partial, loggedIn: boolean, methods: string[], strategy: import('@rm/types').Strategy | '' }, - * filters: object, + * filters: import('@rm/types').AllFilters, * masterfile: import('@rm/types').Masterfile * polling: Record * gymValidDataLimit: number @@ -180,12 +218,14 @@ export const useStore = create( * pokemon: string[], * pokestops: string[], * nests: string[], + * questConditions: Record, * } * manualParams: { * category: string, * id: number | string, * }, * }} UseStatic + * * @type {import("zustand").UseBoundStore>} */ export const useStatic = create((set) => ({ diff --git a/src/services/functions/setDeep.js b/src/services/functions/setDeep.js index 99534911c..fac730fc8 100644 --- a/src/services/functions/setDeep.js +++ b/src/services/functions/setDeep.js @@ -10,17 +10,23 @@ export function setDeep(obj, path, value) { path = path.split('.') } if (path.length > 1) { - const e = path.shift() + const next = path.shift() setDeep( - (obj[e] = - Object.prototype.toString.call(obj[e]) === '[object Object]' - ? { ...obj[e] } + (obj[next] = + Object.prototype.toString.call(obj[next]) === '[object Object]' + ? { ...obj[next] } : {}), path, value, ) } else { - obj[path[0]] = value + obj[path[0]] = + typeof value === 'object' + ? Array.isArray(value) + ? value.slice() + : { ...value } + : value } + return { ...obj } } From ac0cb4fe68e4e5713e3c34c949fcb0839869762d Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:55:41 -0500 Subject: [PATCH 02/48] style: cleanup the advanced filter menu --- .vscode/settings.json | 7 +++--- src/assets/constants.js | 2 ++ .../layout/dialogs/filters/Advanced.jsx | 23 ++++++++----------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a5abe4d60..53038bdd9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,10 @@ { "i18n-ally.localesPaths": [ "packages/locales/lib/human", - "packages/locales/lib/generated" + "packages/locales/lib/generated", + "packages/locales/lib/data" ], - "i18n-ally.keystyle": "flat", + "i18n-ally.keystyle": "auto", "[javascript]": { "editor.autoClosingBrackets": "always", "editor.defaultFormatter": "esbenp.prettier-vscode" @@ -14,4 +15,4 @@ }, "editor.formatOnSave": true, "docwriter.style": "JSDoc" -} \ No newline at end of file +} diff --git a/src/assets/constants.js b/src/assets/constants.js index 36f174a09..d02becf9b 100644 --- a/src/assets/constants.js +++ b/src/assets/constants.js @@ -35,3 +35,5 @@ export const WAYFARER_OPTIONS = /** @type {const} */ ([ export const SPAWNPOINT_TTH = /** @type {const} */ ([0, 1, 2]) export const SLIDER_LABELS = /** @type {const} */ (['min', 'max']) + +export const ADVANCED_ALL = /** @type {const} */ (['', 'all']) diff --git a/src/components/layout/dialogs/filters/Advanced.jsx b/src/components/layout/dialogs/filters/Advanced.jsx index f9c8ac8f6..b26595700 100644 --- a/src/components/layout/dialogs/filters/Advanced.jsx +++ b/src/components/layout/dialogs/filters/Advanced.jsx @@ -8,10 +8,8 @@ import Utility from '@services/Utility' import { useStore, useStatic, useDeepStore } from '@hooks/useStore' import Header from '@components/layout/general/Header' import Footer from '@components/layout/general/Footer' -import { - BoolToggle, - DualBoolToggle, -} from '@components/layout/drawer/BoolToggle' +import { DualBoolToggle } from '@components/layout/drawer/BoolToggle' +import { ADVANCED_ALL, FILTER_SIZES } from '@assets/constants' import { StringFilter } from './StringFilter' import SliderTile from './SliderTile' @@ -70,8 +68,7 @@ export default function AdvancedFilter({ * @param {(typeof filters)[T]} values * @returns */ - const handleChange = (key, values) => - setFilters((prev) => ({ ...prev, [key]: values, all: false })) + const handleChange = (key, values) => setFilters(key, values) const toggleClose = (save = false) => @@ -159,7 +156,7 @@ export default function AdvancedFilter({ key={`${subCat}${each.name}`} disableGutters disablePadding - sx={{ pr: i ? 0 : 2 }} + sx={{ pr: { xs: 0, sm: i ? 0 : 2 } }} > - From 33c6df7204ea8bd7655fec70640ebe423d84c579 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:55:57 -0500 Subject: [PATCH 03/48] fix: disable prop for size & gender --- src/components/layout/dialogs/filters/Gender.jsx | 1 + src/components/layout/dialogs/filters/Size.jsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/layout/dialogs/filters/Gender.jsx b/src/components/layout/dialogs/filters/Gender.jsx index 3ec37838d..db1ffedd7 100644 --- a/src/components/layout/dialogs/filters/Gender.jsx +++ b/src/components/layout/dialogs/filters/Gender.jsx @@ -21,6 +21,7 @@ export function GenderListItem({ field, ...props }) { items={GENDERS} tKey="gender_icon_" field={`${field}.gender`} + disabled={props.disabled} /> ) diff --git a/src/components/layout/dialogs/filters/Size.jsx b/src/components/layout/dialogs/filters/Size.jsx index 7ce4ac3a1..e9398c1a3 100644 --- a/src/components/layout/dialogs/filters/Size.jsx +++ b/src/components/layout/dialogs/filters/Size.jsx @@ -16,7 +16,11 @@ export default function Size({ field, ...props }) { return ( {t('icon_size')} - + ) } From a216235a1e45f723e57925d93e6cd46800b7b562 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:56:09 -0500 Subject: [PATCH 04/48] style: slider tile width --- src/components/layout/dialogs/filters/SliderTile.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/dialogs/filters/SliderTile.jsx b/src/components/layout/dialogs/filters/SliderTile.jsx index 7b956c855..a2f9a8a35 100644 --- a/src/components/layout/dialogs/filters/SliderTile.jsx +++ b/src/components/layout/dialogs/filters/SliderTile.jsx @@ -51,7 +51,6 @@ export default function SliderTile({ const [temp, setTemp] = React.useState(filterValues || []) const [text, setText] = React.useState(filterValues || []) - // console.log({ name, filterValues, min, max }) const handleSliderChange = /** @type {import('@mui/material').SliderProps['onChangeCommitted']} */ ( React.useCallback( @@ -125,6 +124,7 @@ export default function SliderTile({ justifyContent="center" alignItems="center" minWidth={Math.min(window.innerWidth, 260)} + width="100%" > From 03cbddb7cb51f89c1d0280ff3eed846ccf3da163 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:56:36 -0500 Subject: [PATCH 05/48] style: color prop for booltoggle --- src/components/layout/drawer/BoolToggle.jsx | 31 +++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/layout/drawer/BoolToggle.jsx b/src/components/layout/drawer/BoolToggle.jsx index 54447df6d..3842082d2 100644 --- a/src/components/layout/drawer/BoolToggle.jsx +++ b/src/components/layout/drawer/BoolToggle.jsx @@ -18,6 +18,7 @@ import { fromSnakeCase } from '@services/functions/fromSnakeCase' * disabled?: boolean, * children?: React.ReactNode, * align?: import('@mui/material').TypographyProps['align'] + * switchColor?: import('@mui/material').SwitchProps['color'] * } & import('@mui/material').ListItemProps} BoolToggleProps */ @@ -28,13 +29,14 @@ export function BoolBase({ disabled = false, children, align, + switchColor, ...props }) { const { t } = useTranslation() const [value, setValue] = useDeepStore(field, false) const onChange = /** @type {import('@mui/material').SwitchProps['onChange']} */ ( - React.useCallback((_, checked) => setValue(checked), [field]) + React.useCallback((_, checked) => setValue(checked), [field, setValue]) ) return ( @@ -42,7 +44,12 @@ export function BoolBase({ {t(label, t(Utility.camelToSnake(label), fromSnakeCase(label)))} - + ) } @@ -58,15 +65,17 @@ export function DualBoolToggle({ items, field, ...props }) { {items.map((item) => ( - + {item && ( + + )} ))} From 1cf4955c843253e9429155a3ba3a785808f40160 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Fri, 5 Jan 2024 10:56:51 -0500 Subject: [PATCH 06/48] fix: disable prop for multiselector --- src/components/layout/drawer/MultiSelector.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/layout/drawer/MultiSelector.jsx b/src/components/layout/drawer/MultiSelector.jsx index d7bd0d968..9827ae675 100644 --- a/src/components/layout/drawer/MultiSelector.jsx +++ b/src/components/layout/drawer/MultiSelector.jsx @@ -13,15 +13,22 @@ import { setDeep } from '@services/functions/setDeep' * items: readonly (string | number)[], * tKey?: string, * allowNone?: boolean, + * disabled?: boolean * }} props * @returns */ -export function MultiSelector({ field, items, tKey, allowNone = false }) { +export function MultiSelector({ + field, + disabled, + items, + tKey, + allowNone = false, +}) { const { t } = useTranslation() const value = useStore((s) => dlv(s, field)) return ( - + {items.map((item) => ( - - - {t('tutorial_save')} - + ) : ( + + - {toggleHelp ? ( - - - - ) : ( - - - - )} - - - + )} + + ) } diff --git a/src/components/layout/general/Menu.jsx b/src/components/layout/general/Menu.jsx index 95891c400..68e5a80a2 100644 --- a/src/components/layout/general/Menu.jsx +++ b/src/components/layout/general/Menu.jsx @@ -13,7 +13,6 @@ import Header from '@components/layout/general/Header' import Footer from '@components/layout/general/Footer' import { applyToAll } from '@services/filtering/applyToAll' -import SlotSelection from '../dialogs/filters/SlotSelection' import OptionsContainer from '../dialogs/filters/OptionsContainer' import Help from '../dialogs/tutorial/Advanced' import { VirtualGrid } from './VirtualGrid' @@ -49,10 +48,6 @@ export default function Menu({ const menus = useStore((state) => state.menus) const [filterDrawer, setFilterDrawer] = useState(false) - const [slotsMenu, setSlotsMenu] = useState({ - open: false, - id: 0, - }) const [helpDialog, setHelpDialog] = useState(false) const { filteredObj, filteredArr, count } = useFilter( @@ -76,26 +71,6 @@ export default function Menu({ [], ) - const toggleSlotsMenu = (open, id, newFilters) => (event) => { - if ( - event.type === 'keydown' && - (event.key === 'Tab' || event.key === 'Shift') - ) { - return - } - if (open) { - setSlotsMenu({ - open, - id, - }) - } else if (newFilters) { - setSlotsMenu({ open }) - // setTempFilters({ ...newFilters }) - } else { - setSlotsMenu({ open }) - } - } - const Options = React.useMemo( () => ( )} - - - setHelpDialog(false)}> setHelpDialog(!helpDialog)} From 52690be10a3accae108c02803c51b14cee2b0111 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:17:15 -0500 Subject: [PATCH 43/48] fix: cleanups --- src/assets/constants.js | 8 ++++++ src/components/QueryData.jsx | 3 +-- src/components/layout/dialogs/Search.jsx | 2 +- .../layout/dialogs/filters/Advanced.jsx | 26 +++++++++++++------ .../layout/dialogs/filters/SlotSelection.jsx | 2 +- .../layout/dialogs/profile/LinkAccounts.jsx | 2 +- .../layout/dialogs/profile/Permissions.jsx | 3 +-- .../layout/dialogs/scanner/ScanDialog.jsx | 3 ++- .../dialogs/scanner/scanNext/PopupContent.jsx | 8 +++--- src/components/layout/drawer/SelectorItem.jsx | 17 ++++-------- src/hooks/useStore.js | 1 - 11 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/assets/constants.js b/src/assets/constants.js index 1fd1e4705..727401b55 100644 --- a/src/assets/constants.js +++ b/src/assets/constants.js @@ -43,3 +43,11 @@ 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/components/QueryData.jsx b/src/components/QueryData.jsx index 068b9bc22..5599fe078 100644 --- a/src/components/QueryData.jsx +++ b/src/components/QueryData.jsx @@ -9,14 +9,13 @@ 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) { diff --git a/src/components/layout/dialogs/Search.jsx b/src/components/layout/dialogs/Search.jsx index a103a6adc..9ea804de8 100644 --- a/src/components/layout/dialogs/Search.jsx +++ b/src/components/layout/dialogs/Search.jsx @@ -286,7 +286,7 @@ export default function Search() { }} > -
+
)} {category === 'pokestops' && } - + {category === 'pokemon' || category === 'pokestops' ? ( + + ) : ( + + )} )} diff --git a/src/components/layout/dialogs/filters/SlotSelection.jsx b/src/components/layout/dialogs/filters/SlotSelection.jsx index e07bc3e08..0135b829e 100644 --- a/src/components/layout/dialogs/filters/SlotSelection.jsx +++ b/src/components/layout/dialogs/filters/SlotSelection.jsx @@ -120,7 +120,7 @@ export default function SlotSelection() { /> - + {teamId !== '0' && } {teamId !== '0' && slots.map((each) => ( diff --git a/src/components/layout/dialogs/profile/LinkAccounts.jsx b/src/components/layout/dialogs/profile/LinkAccounts.jsx index d361ad41a..7b69af7ee 100644 --- a/src/components/layout/dialogs/profile/LinkAccounts.jsx +++ b/src/components/layout/dialogs/profile/LinkAccounts.jsx @@ -34,7 +34,7 @@ export function LinkAccounts() { return ( <> - {['discord', 'telegram'].map((method, i) => { + {METHODS.map((method, i) => { if (!auth.methods.includes(method)) return null const Component = i ? ( diff --git a/src/components/layout/dialogs/profile/Permissions.jsx b/src/components/layout/dialogs/profile/Permissions.jsx index ccc5f4558..ecda2c3c1 100644 --- a/src/components/layout/dialogs/profile/Permissions.jsx +++ b/src/components/layout/dialogs/profile/Permissions.jsx @@ -10,8 +10,7 @@ import Typography from '@mui/material/Typography' import { useStatic } from '@hooks/useStore' import Utility from '@services/Utility' - -const ALWAYS_EXCLUDED = new Set(['donor', 'blockedGuildNames', 'admin']) +import { ALWAYS_EXCLUDED } from '@assets/constants' export function UserPermissions() { const perms = useStatic((s) => s.auth.perms) diff --git a/src/components/layout/dialogs/scanner/ScanDialog.jsx b/src/components/layout/dialogs/scanner/ScanDialog.jsx index e2873bd41..641e9433d 100644 --- a/src/components/layout/dialogs/scanner/ScanDialog.jsx +++ b/src/components/layout/dialogs/scanner/ScanDialog.jsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import Header from '@components/layout/general/Header' import Footer from '@components/layout/general/Footer' +import { SCAN_MODES } from '@assets/constants' import { useScanStore } from './store' @@ -28,7 +29,7 @@ export default function ScanDialog() { return (
diff --git a/src/components/layout/dialogs/scanner/scanNext/PopupContent.jsx b/src/components/layout/dialogs/scanner/scanNext/PopupContent.jsx index f3acabf6e..cb68e69d1 100644 --- a/src/components/layout/dialogs/scanner/scanNext/PopupContent.jsx +++ b/src/components/layout/dialogs/scanner/scanNext/PopupContent.jsx @@ -3,16 +3,16 @@ import * as React from 'react' import { Button, ButtonGroup, ListItem } from '@mui/material' import { useTranslation } from 'react-i18next' -import { useScanStore } from '../store' +import { SCAN_SIZES } from '@assets/constants' -const SIZES = /** @type {const} */ (['S', 'M', 'XL']) +import { useScanStore } from '../store' export function ScanNextPopup() { const { t } = useTranslation() const scanNextSize = useScanStore((s) => s.scanNextSize) const setSize = React.useCallback( - (/** @type {typeof SIZES[number]} */ size) => () => { + (/** @type {typeof SCAN_SIZES[number]} */ size) => () => { useScanStore.setState({ scanNextSize: size }) }, [], @@ -21,7 +21,7 @@ export function ScanNextPopup() { return ( - {SIZES.map((size) => ( + {SCAN_SIZES.map((size) => (