From 7ed1394fbaf41da16e672b1ac99e26e3f8e818cc Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sat, 21 Jun 2025 13:44:26 +0300 Subject: [PATCH] Migrate Comboboxes to react-aria-components (#2408) * Initial * wip * AnyWeapon * del * wip * import stuff * gearselect * brand images * wip * wip * art * Remove old * Fix tournament map pool link * Simplify GearSelect * convert to todo --- app/components/Combobox.tsx | 452 ------------------ app/components/GearSelect.tsx | 140 ++++++ app/components/MapPoolSelector.tsx | 83 +--- app/components/WeaponSelect.module.css | 48 ++ app/components/WeaponSelect.tsx | 280 +++++++++++ app/components/elements/Select.module.css | 15 + app/components/elements/Select.tsx | 94 +++- app/components/elements/UserSearch.tsx | 1 + app/features/art/components/TagSelect.tsx | 34 ++ app/features/art/routes/art.new.tsx | 30 +- app/features/art/routes/art.tsx | 18 +- .../build-analyzer/routes/analyzer.tsx | 13 +- .../calendar/CalendarRepository.server.ts | 49 -- .../calendar/loaders/calendar.new.server.ts | 2 - app/features/calendar/routes/calendar.$id.tsx | 8 +- app/features/calendar/routes/calendar.new.tsx | 6 +- .../calendar/routes/map-pool-events.ts | 12 - app/features/lfg/components/LFGFilters.tsx | 16 +- .../map-list-generator/loaders/maps.server.ts | 29 -- .../map-list-generator/routes/maps.tsx | 52 +- .../routes/object-damage-calculator.tsx | 21 +- .../sendouq-match/routes/q.match.$id.tsx | 132 +---- .../sendouq-settings/routes/q.settings.tsx | 40 +- app/features/sendouq/q-hooks.ts | 45 -- app/features/sendouq/q.css | 17 +- .../top-search/components/Placements.tsx | 7 +- .../routes/to.$id.subs.new.tsx | 46 +- .../tournament/routes/to.$id.register.tsx | 8 +- .../u.$identifier.builds.new.server.ts | 23 +- .../routes/u.$identifier.builds.new.tsx | 99 ++-- .../user-page/routes/u.$identifier.edit.tsx | 88 ++-- app/features/vods/routes/vods.new.tsx | 61 +-- app/features/vods/routes/vods.tsx | 21 +- app/hooks/swr.ts | 15 - app/modules/in-game-lists/brand-ids.ts | 23 + app/modules/in-game-lists/gear-ids.ts | 251 +++++++--- app/modules/in-game-lists/types.ts | 3 + app/modules/in-game-lists/utils.ts | 31 ++ app/modules/in-game-lists/weapon-alt-names.ts | 6 + app/routes.ts | 1 - app/styles/common.css | 77 --- app/styles/u.$identifier.module.css | 4 - app/styles/utils.css | 6 +- app/utils/playwright.ts | 31 +- app/utils/urls.ts | 14 +- e2e/analyzer.spec.ts | 6 +- e2e/builds.spec.ts | 57 ++- e2e/vods.spec.ts | 14 +- locales/da/analyzer.json | 1 + locales/da/art.json | 3 +- locales/da/common.json | 6 +- locales/da/game-misc.json | 23 +- locales/da/org.json | 3 +- locales/de/analyzer.json | 1 + locales/de/art.json | 3 +- locales/de/common.json | 6 +- locales/de/game-misc.json | 23 +- locales/de/org.json | 3 +- locales/en/analyzer.json | 1 + locales/en/art.json | 3 +- locales/en/common.json | 6 +- locales/en/game-misc.json | 23 +- locales/es-ES/analyzer.json | 1 + locales/es-ES/art.json | 3 +- locales/es-ES/common.json | 6 +- locales/es-ES/game-misc.json | 23 +- locales/es-ES/org.json | 3 +- locales/es-US/analyzer.json | 1 + locales/es-US/art.json | 3 +- locales/es-US/common.json | 6 +- locales/es-US/game-misc.json | 23 +- locales/es-US/org.json | 3 +- locales/fr-CA/analyzer.json | 1 + locales/fr-CA/art.json | 3 +- locales/fr-CA/common.json | 6 +- locales/fr-CA/game-misc.json | 23 +- locales/fr-CA/org.json | 3 +- locales/fr-EU/analyzer.json | 1 + locales/fr-EU/art.json | 3 +- locales/fr-EU/common.json | 6 +- locales/fr-EU/game-misc.json | 23 +- locales/fr-EU/org.json | 3 +- locales/he/analyzer.json | 1 + locales/he/art.json | 3 +- locales/he/common.json | 6 +- locales/he/game-misc.json | 31 +- locales/he/org.json | 3 +- locales/it/analyzer.json | 1 + locales/it/art.json | 3 +- locales/it/common.json | 6 +- locales/it/game-misc.json | 23 +- locales/it/org.json | 3 +- locales/ja/analyzer.json | 1 + locales/ja/art.json | 3 +- locales/ja/common.json | 6 +- locales/ja/game-misc.json | 23 +- locales/ja/org.json | 3 +- locales/ko/analyzer.json | 1 + locales/ko/art.json | 3 +- locales/ko/common.json | 6 +- locales/ko/game-misc.json | 23 +- locales/ko/org.json | 3 +- locales/nl/analyzer.json | 1 + locales/nl/art.json | 3 +- locales/nl/common.json | 6 +- locales/nl/game-misc.json | 23 +- locales/nl/org.json | 3 +- locales/pl/analyzer.json | 1 + locales/pl/art.json | 3 +- locales/pl/common.json | 6 +- locales/pl/game-misc.json | 31 +- locales/pl/org.json | 3 +- locales/pt-BR/analyzer.json | 1 + locales/pt-BR/art.json | 3 +- locales/pt-BR/common.json | 6 +- locales/pt-BR/game-misc.json | 31 +- locales/pt-BR/org.json | 3 +- locales/ru/analyzer.json | 1 + locales/ru/art.json | 3 +- locales/ru/common.json | 6 +- locales/ru/game-misc.json | 23 +- locales/ru/gear.json | 4 +- locales/ru/org.json | 3 +- locales/zh/analyzer.json | 1 + locales/zh/art.json | 3 +- locales/zh/common.json | 6 +- locales/zh/game-misc.json | 23 +- locales/zh/org.json | 3 +- package-lock.json | 55 --- package.json | 2 - public/static-assets/img/brands/B00.avif | Bin 0 -> 1467 bytes public/static-assets/img/brands/B00.png | Bin 0 -> 2052 bytes public/static-assets/img/brands/B01.avif | Bin 0 -> 993 bytes public/static-assets/img/brands/B01.png | Bin 0 -> 1676 bytes public/static-assets/img/brands/B02.avif | Bin 0 -> 2016 bytes public/static-assets/img/brands/B02.png | Bin 0 -> 3426 bytes public/static-assets/img/brands/B03.avif | Bin 0 -> 1370 bytes public/static-assets/img/brands/B03.png | Bin 0 -> 2657 bytes public/static-assets/img/brands/B04.avif | Bin 0 -> 1215 bytes public/static-assets/img/brands/B04.png | Bin 0 -> 1819 bytes public/static-assets/img/brands/B05.avif | Bin 0 -> 925 bytes public/static-assets/img/brands/B05.png | Bin 0 -> 1262 bytes public/static-assets/img/brands/B06.avif | Bin 0 -> 1381 bytes public/static-assets/img/brands/B06.png | Bin 0 -> 2362 bytes public/static-assets/img/brands/B07.avif | Bin 0 -> 1852 bytes public/static-assets/img/brands/B07.png | Bin 0 -> 2908 bytes public/static-assets/img/brands/B08.avif | Bin 0 -> 932 bytes public/static-assets/img/brands/B08.png | Bin 0 -> 872 bytes public/static-assets/img/brands/B09.avif | Bin 0 -> 1015 bytes public/static-assets/img/brands/B09.png | Bin 0 -> 1694 bytes public/static-assets/img/brands/B10.avif | Bin 0 -> 1316 bytes public/static-assets/img/brands/B10.png | Bin 0 -> 1196 bytes public/static-assets/img/brands/B11.avif | Bin 0 -> 1044 bytes public/static-assets/img/brands/B11.png | Bin 0 -> 844 bytes public/static-assets/img/brands/B15.avif | Bin 0 -> 1415 bytes public/static-assets/img/brands/B15.png | Bin 0 -> 1698 bytes public/static-assets/img/brands/B16.avif | Bin 0 -> 1768 bytes public/static-assets/img/brands/B16.png | Bin 0 -> 2421 bytes public/static-assets/img/brands/B17.avif | Bin 0 -> 675 bytes public/static-assets/img/brands/B17.png | Bin 0 -> 510 bytes public/static-assets/img/brands/B18.avif | Bin 0 -> 1189 bytes public/static-assets/img/brands/B18.png | Bin 0 -> 2054 bytes public/static-assets/img/brands/B19.avif | Bin 0 -> 1133 bytes public/static-assets/img/brands/B19.png | Bin 0 -> 1956 bytes public/static-assets/img/brands/B20.avif | Bin 0 -> 902 bytes public/static-assets/img/brands/B20.png | Bin 0 -> 1701 bytes public/static-assets/img/brands/B97.avif | Bin 0 -> 1780 bytes public/static-assets/img/brands/B97.png | Bin 0 -> 4215 bytes public/static-assets/img/brands/B98.avif | Bin 0 -> 1745 bytes public/static-assets/img/brands/B98.png | Bin 0 -> 2683 bytes public/static-assets/img/brands/B99.avif | Bin 0 -> 1039 bytes public/static-assets/img/brands/B99.png | Bin 0 -> 1381 bytes public/static-assets/img/layout/takoroka.avif | Bin 1504 -> 0 bytes public/static-assets/img/layout/takoroka.png | Bin 2135 -> 0 bytes public/static-assets/img/layout/tentatek.avif | Bin 1493 -> 0 bytes public/static-assets/img/layout/tentatek.png | Bin 2585 -> 0 bytes scripts/create-gear-json.ts | 24 +- scripts/create-misc-json.ts | 22 +- 178 files changed, 1686 insertions(+), 1547 deletions(-) delete mode 100644 app/components/Combobox.tsx create mode 100644 app/components/GearSelect.tsx create mode 100644 app/components/WeaponSelect.module.css create mode 100644 app/components/WeaponSelect.tsx create mode 100644 app/features/art/components/TagSelect.tsx delete mode 100644 app/features/calendar/routes/map-pool-events.ts delete mode 100644 app/features/map-list-generator/loaders/maps.server.ts delete mode 100644 app/features/sendouq/q-hooks.ts create mode 100644 app/modules/in-game-lists/brand-ids.ts create mode 100644 public/static-assets/img/brands/B00.avif create mode 100644 public/static-assets/img/brands/B00.png create mode 100644 public/static-assets/img/brands/B01.avif create mode 100644 public/static-assets/img/brands/B01.png create mode 100644 public/static-assets/img/brands/B02.avif create mode 100644 public/static-assets/img/brands/B02.png create mode 100644 public/static-assets/img/brands/B03.avif create mode 100644 public/static-assets/img/brands/B03.png create mode 100644 public/static-assets/img/brands/B04.avif create mode 100644 public/static-assets/img/brands/B04.png create mode 100644 public/static-assets/img/brands/B05.avif create mode 100644 public/static-assets/img/brands/B05.png create mode 100644 public/static-assets/img/brands/B06.avif create mode 100644 public/static-assets/img/brands/B06.png create mode 100644 public/static-assets/img/brands/B07.avif create mode 100644 public/static-assets/img/brands/B07.png create mode 100644 public/static-assets/img/brands/B08.avif create mode 100644 public/static-assets/img/brands/B08.png create mode 100644 public/static-assets/img/brands/B09.avif create mode 100644 public/static-assets/img/brands/B09.png create mode 100644 public/static-assets/img/brands/B10.avif create mode 100644 public/static-assets/img/brands/B10.png create mode 100644 public/static-assets/img/brands/B11.avif create mode 100644 public/static-assets/img/brands/B11.png create mode 100644 public/static-assets/img/brands/B15.avif create mode 100644 public/static-assets/img/brands/B15.png create mode 100644 public/static-assets/img/brands/B16.avif create mode 100644 public/static-assets/img/brands/B16.png create mode 100644 public/static-assets/img/brands/B17.avif create mode 100644 public/static-assets/img/brands/B17.png create mode 100644 public/static-assets/img/brands/B18.avif create mode 100644 public/static-assets/img/brands/B18.png create mode 100644 public/static-assets/img/brands/B19.avif create mode 100644 public/static-assets/img/brands/B19.png create mode 100644 public/static-assets/img/brands/B20.avif create mode 100644 public/static-assets/img/brands/B20.png create mode 100644 public/static-assets/img/brands/B97.avif create mode 100644 public/static-assets/img/brands/B97.png create mode 100644 public/static-assets/img/brands/B98.avif create mode 100644 public/static-assets/img/brands/B98.png create mode 100644 public/static-assets/img/brands/B99.avif create mode 100644 public/static-assets/img/brands/B99.png delete mode 100644 public/static-assets/img/layout/takoroka.avif delete mode 100644 public/static-assets/img/layout/takoroka.png delete mode 100644 public/static-assets/img/layout/tentatek.avif delete mode 100644 public/static-assets/img/layout/tentatek.png diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx deleted file mode 100644 index 7657ebf87..000000000 --- a/app/components/Combobox.tsx +++ /dev/null @@ -1,452 +0,0 @@ -import { Combobox as HeadlessCombobox } from "@headlessui/react"; -import clsx from "clsx"; -import Fuse, { type IFuseOptions } from "fuse.js"; -import * as React from "react"; -import { useTranslation } from "react-i18next"; -import type { GearType } from "~/db/tables"; -import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; -import { useAllEventsWithMapPools } from "~/hooks/swr"; -import { - clothesGearIds, - headGearIds, - shoesGearIds, -} from "~/modules/in-game-lists/gear-ids"; -import type { MainWeaponId } from "~/modules/in-game-lists/types"; -import { weaponAltNames } from "~/modules/in-game-lists/weapon-alt-names"; -import { - mainWeaponIds, - subWeaponIds, - weaponCategories, -} from "~/modules/in-game-lists/weapon-ids"; -import { - nonBombSubWeaponIds, - nonDamagingSpecialWeaponIds, - specialWeaponIds, -} from "~/modules/in-game-lists/weapon-ids"; -import type { Unpacked } from "~/utils/types"; -import { - gearImageUrl, - mainWeaponImageUrl, - specialWeaponImageUrl, - subWeaponImageUrl, -} from "~/utils/urls"; -import { Image } from "./Image"; - -const MAX_RESULTS_SHOWN = 6; - -interface ComboboxBaseOption { - label: string; - /** Alternative text other than label to match by */ - alt?: string[]; - value: string; - imgPath?: string; -} - -type ComboboxOption = ComboboxBaseOption & T; -interface ComboboxProps { - options: ComboboxOption[]; - quickSelectOptions?: ComboboxOption[]; - inputName: string; - placeholder: string; - className?: string; - id?: string; - isLoading?: boolean; - required?: boolean; - value?: ComboboxOption | null; - initialValue: ComboboxOption | null; - onChange?: (selectedOption: ComboboxOption | null) => void; - fullWidth?: boolean; - nullable?: true; - fuseOptions?: IFuseOptions>; -} - -export function Combobox< - T extends Record, ->({ - options, - quickSelectOptions, - inputName, - placeholder, - value, - initialValue, - onChange, - required, - className, - id, - nullable, - isLoading = false, - fullWidth = false, - fuseOptions = {}, -}: ComboboxProps) { - const { t } = useTranslation(); - const buttonRef = React.useRef(null); - const inputRef = React.useRef(null); - - const [_selectedOption, setSelectedOption] = React.useState | null>(initialValue); - const [query, setQuery] = React.useState(""); - - const fuse = new Fuse(options, { - ...fuseOptions, - keys: ["label", "alt"], - }); - - const filteredOptions = (() => { - if (!query) { - if (quickSelectOptions) return quickSelectOptions; - - return []; - } - - return fuse - .search(query) - .slice(0, MAX_RESULTS_SHOWN) - .map((res) => res.item); - })(); - - const noMatches = filteredOptions.length === 0; - - const displayValue = (option: Unpacked) => { - return option?.label ?? ""; - }; - - const selectedOption = value ?? _selectedOption; - - const showComboboxOptions = () => { - if (!quickSelectOptions || quickSelectOptions.length === 0) return; - - buttonRef.current?.click(); - }; - - return ( -
- { - onChange?.(selected); - setSelectedOption(selected); - // https://github.com/tailwindlabs/headlessui/issues/1555 - // note that this still seems to be a problem despite what the issue says - setTimeout(() => inputRef.current?.blur(), 0); - }} - name={inputName} - disabled={!selectedOption && isLoading} - // TODO: remove hack that prevents TS from freaking out. probably related: https://github.com/tailwindlabs/headlessui/issues/1895 - nullable={nullable as true} - > - setQuery(event.target.value)} - placeholder={isLoading ? t("actions.loading") : placeholder} - className={clsx("combobox-input", className, { - fullWidth, - })} - defaultValue={initialValue} - displayValue={displayValue} - data-testid={`${inputName}-combobox-input`} - id={id} - required={required} - autoComplete="off" - onFocus={showComboboxOptions} - ref={inputRef} - /> - - {isLoading ? ( -
{t("actions.loading")}
- ) : noMatches ? ( -
- {t("forms.errors.noSearchMatches")}{" "} - 🤔 -
- ) : ( - filteredOptions.map((option) => ( - - {({ active }) => ( -
  • - {option.imgPath && ( - - )} - {option.label} -
  • - )} -
    - )) - )} -
    - -
    -
    - ); -} - -export function WeaponCombobox({ - id, - required, - className, - inputName, - onChange, - initialWeaponId, - weaponIdsToOmit, - fullWidth, - nullable, - value, - quickSelectWeaponIds, -}: Pick< - ComboboxProps, - | "inputName" - | "onChange" - | "className" - | "id" - | "required" - | "fullWidth" - | "nullable" -> & { - initialWeaponId?: (typeof mainWeaponIds)[number]; - weaponIdsToOmit?: Set; - value?: MainWeaponId | null; - /** Weapons to show when there is focus but no query */ - quickSelectWeaponIds?: MainWeaponId[]; -}) { - const { t, i18n } = useTranslation("weapons"); - - const alt = (id: (typeof mainWeaponIds)[number]) => { - const result: string[] = []; - - if (i18n.language !== "en") { - result.push(t(`MAIN_${id}`, { lng: "en" })); - } - - const altNames = weaponAltNames.get(id); - if (typeof altNames === "string") { - result.push(altNames); - } else if (Array.isArray(altNames)) { - result.push(...altNames); - } - - return result; - }; - const idToWeapon = (id: (typeof mainWeaponIds)[number]) => ({ - value: String(id), - label: t(`MAIN_${id}`), - imgPath: mainWeaponImageUrl(id), - alt: alt(id), - }); - - const options = mainWeaponIds - .filter((id) => !weaponIdsToOmit?.has(id)) - .map(idToWeapon); - - const quickSelectOptions = quickSelectWeaponIds?.flatMap((weaponId) => { - return options.find((option) => option.value === String(weaponId)) ?? []; - }); - - return ( - - ); -} - -export function AllWeaponCombobox({ - id, - inputName, - onChange, - fullWidth, -}: Pick< - ComboboxProps, - "inputName" | "onChange" | "id" | "fullWidth" ->) { - const { t } = useTranslation("weapons"); - - const options = () => { - const result: ComboboxProps< - Record - >["options"] = []; - - for (const mainWeaponId of mainWeaponIds) { - result.push({ - value: `MAIN_${mainWeaponId}`, - label: t(`MAIN_${mainWeaponId}`), - imgPath: mainWeaponImageUrl(mainWeaponId), - }); - } - - for (const subWeaponId of subWeaponIds) { - if (nonBombSubWeaponIds.includes(subWeaponId)) continue; - - result.push({ - value: `SUB_${subWeaponId}`, - label: t(`SUB_${subWeaponId}`), - imgPath: subWeaponImageUrl(subWeaponId), - }); - } - - for (const specialWeaponId of specialWeaponIds) { - if (nonDamagingSpecialWeaponIds.includes(specialWeaponId)) continue; - - result.push({ - value: `SPECIAL_${specialWeaponId}`, - label: t(`SPECIAL_${specialWeaponId}`), - imgPath: specialWeaponImageUrl(specialWeaponId), - }); - } - - return result; - }; - - return ( - - ); -} - -export function GearCombobox({ - id, - required, - className, - inputName, - onChange, - gearType, - initialGearId, - nullable, -}: Pick< - ComboboxProps, - "inputName" | "onChange" | "className" | "id" | "required" | "nullable" -> & { gearType: GearType; initialGearId?: number }) { - const { t } = useTranslation("gear"); - - const translationPrefix = - gearType === "HEAD" ? "H" : gearType === "CLOTHES" ? "C" : "S"; - const ids = - gearType === "HEAD" - ? headGearIds - : gearType === "CLOTHES" - ? clothesGearIds - : shoesGearIds; - - const idToGear = (id: (typeof ids)[number]) => ({ - value: String(id), - label: t(`${translationPrefix}_${id}` as any), - imgPath: gearImageUrl(gearType, id), - }); - - return ( - - ); -} - -const mapPoolEventToOption = ( - e: SerializedMapPoolEvent, -): ComboboxOption> => ({ - serializedMapPool: e.serializedMapPool, - label: e.name, - value: e.id.toString(), -}); - -type MapPoolEventsComboboxProps = Pick< - ComboboxProps>, - "inputName" | "className" | "id" | "required" -> & { - initialEvent?: SerializedMapPoolEvent; - onChange: (event: SerializedMapPoolEvent | null) => void; -}; - -export function MapPoolEventsCombobox({ - id, - required, - className, - inputName, - onChange, - initialEvent, -}: MapPoolEventsComboboxProps) { - const { t } = useTranslation(); - const { events, isLoading, isError } = useAllEventsWithMapPools(); - - const options = React.useMemo( - () => (events ? events.map(mapPoolEventToOption) : []), - [events], - ); - - // this is important so that we don't trigger the reset to the initialEvent every time - const initialOption = React.useMemo( - () => initialEvent && mapPoolEventToOption(initialEvent), - [initialEvent], - ); - - if (isError) { - return ( -
    {t("errors.genericReload")}
    - ); - } - - return ( - { - onChange( - e && { - id: Number.parseInt(e.value, 10), - name: e.label, - serializedMapPool: e.serializedMapPool, - }, - ); - }} - className={className} - id={id} - required={required} - isLoading={isLoading} - fullWidth - /> - ); -} diff --git a/app/components/GearSelect.tsx b/app/components/GearSelect.tsx new file mode 100644 index 000000000..59cceb5e5 --- /dev/null +++ b/app/components/GearSelect.tsx @@ -0,0 +1,140 @@ +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { Image } from "~/components/Image"; +import { + SendouSelect, + SendouSelectItem, + SendouSelectItemSection, +} from "~/components/elements/Select"; +import type { GearType } from "~/db/tables"; +import { brandIds } from "~/modules/in-game-lists/brand-ids"; +import { + clothesGearBrandGrouped, + headGearBrandGrouped, + shoesGearBrandGrouped, +} from "~/modules/in-game-lists/gear-ids"; +import type { BrandId } from "~/modules/in-game-lists/types"; +import { brandImageUrl, gearImageUrl } from "~/utils/urls"; + +import styles from "./WeaponSelect.module.css"; + +interface GearSelectProps { + label?: string; + value?: number | (Clearable extends true ? null : never); + initialValue?: number; + onChange?: ( + weaponId: number | (Clearable extends true ? null : never), + ) => void; + clearable?: Clearable; + type: GearType; +} + +export function GearSelect({ + label, + value, + initialValue, + onChange, + clearable, + type, +}: GearSelectProps) { + const { t } = useTranslation(["common"]); + const items = useGearItems(type); + + return ( + onChange?.(value as any)} + clearable={clearable} + data-testid={`${type}-gear-select`} + > + {({ key, items: gear, brandId, idx }) => ( + + } + key={key} + > + {gear.map(({ id, name }) => ( + +
    + + + {name} + +
    +
    + ))} +
    + )} +
    + ); +} + +function CategoryHeading({ + className, + brandId, +}: { + className?: string; + brandId: BrandId; +}) { + const { t } = useTranslation(["game-misc"]); + + return ( +
    + + {t(`game-misc:BRAND_${brandId}` as any)} +
    +
    + ); +} + +function useGearItems(type: GearType) { + const { t } = useTranslation(["gear", "game-misc"]); + + const translationPrefix = + type === "HEAD" ? "H" : type === "CLOTHES" ? "C" : "S"; + + const groupedGear = + type === "HEAD" + ? headGearBrandGrouped + : type === "CLOTHES" + ? clothesGearBrandGrouped + : shoesGearBrandGrouped; + + const items = brandIds.map((brandId, idx) => { + const items = groupedGear[brandId] || []; + + return { + brandId, + key: brandId, + idx, + items: items.map((gearId) => ({ + id: gearId, + name: t(`${translationPrefix}_${gearId}` as any), + })), + }; + }); + + return items; +} diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index a34325193..b6c57b65d 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -3,7 +3,6 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import { Image } from "~/components/Image"; import type { Tables } from "~/db/tables"; -import type { SerializedMapPoolEvent } from "~/features/calendar/routes/map-pool-events"; import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; import { modesShort } from "~/modules/in-game-lists/modes"; @@ -12,7 +11,6 @@ import type { ModeShort, StageId } from "~/modules/in-game-lists/types"; import { split, startsWith } from "~/utils/strings"; import { assertType } from "~/utils/types"; import { modeImageUrl, stageImageUrl } from "~/utils/urls"; -import { MapPoolEventsCombobox } from "./Combobox"; import { SendouButton } from "./elements/Button"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import { CrossIcon } from "./icons/Cross"; @@ -28,8 +26,6 @@ export type MapPoolSelectorProps = { event?: Pick, ) => void; className?: string; - recentEvents?: SerializedMapPoolEvent[]; - initialEvent?: Pick; title?: string; modesToInclude?: ModeShort[]; info?: React.ReactNode; @@ -45,8 +41,6 @@ export function MapPoolSelector({ handleMapPoolChange, handleRemoval, className, - recentEvents, - initialEvent, title, modesToInclude, info, @@ -57,15 +51,7 @@ export function MapPoolSelector({ const { t } = useTranslation(); const [template, setTemplate] = React.useState( - initialEvent ? "event" : detectTemplate(mapPool), - ); - - const [initialSerializedEvent, setInitialSerializedEvent] = React.useState( - (): SerializedMapPoolEvent | undefined => - initialEvent && { - ...initialEvent, - serializedMapPool: mapPool.serialized, - }, + detectTemplate(mapPool), ); const handleStageModesChange = (newMapPool: MapPool) => { @@ -85,14 +71,6 @@ export function MapPoolSelector({ return; } - if (template === "event") { - // If the user selected the "event" option, the _initial_ event passed via - // props is likely not the current state and should not be prefilled - // anymore. - setInitialSerializedEvent(undefined); - return; - } - if (startsWith(template, "preset:")) { const [, presetId] = split(template, ":"); @@ -100,17 +78,6 @@ export function MapPoolSelector({ return; } - if (startsWith(template, "recent-event:")) { - const [, eventId] = split(template, ":"); - - const event = recentEvents?.find((e) => e.id.toString() === eventId); - - if (event) { - handleMapPoolChange(new MapPool(event.serializedMapPool), event); - } - return; - } - assertType(); }; @@ -141,14 +108,7 @@ export function MapPoolSelector({ - {template === "event" && ( - - )}
    )} {info} @@ -355,11 +315,7 @@ type MapModePresetId = "ANARCHY" | "ALL" | ModeShort; const presetIds: MapModePresetId[] = ["ANARCHY", "ALL", ...modesShort]; -type MapPoolTemplateValue = - | "none" - | `preset:${MapModePresetId}` - | `recent-event:${string}` - | "event"; +type MapPoolTemplateValue = "none" | `preset:${MapModePresetId}`; function detectTemplate(mapPool: MapPool): MapPoolTemplateValue { for (const presetId of presetIds) { @@ -393,7 +349,6 @@ function MapPoolTemplateSelect({ }} > - {(["ANARCHY", "ALL"] as const).map((presetId) => (