From d1b13676511f4c84475525eb04492d4a5ed885a8 Mon Sep 17 00:00:00 2001 From: Remmy Cat Stock <3317423+remmycat@users.noreply.github.com> Date: Wed, 26 Oct 2022 18:00:28 +0200 Subject: [PATCH] Add event search to map pool templates --- app/components/Combobox.tsx | 232 ++++++++++++------ app/components/MapPoolSelector.tsx | 69 +++++- .../calendar/findAllEventsWithMapPools.sql | 18 ++ app/db/models/calendar/queries.server.ts | 16 ++ app/hooks/swr.ts | 22 +- app/routes/calendar/map-pool-events.ts | 12 + app/routes/maps.tsx | 1 + app/styles/common.css | 12 + app/utils/urls.ts | 3 + public/locales/de/common.json | 6 + public/locales/en/common.json | 6 + 11 files changed, 315 insertions(+), 82 deletions(-) create mode 100644 app/db/models/calendar/findAllEventsWithMapPools.sql create mode 100644 app/routes/calendar/map-pool-events.ts diff --git a/app/components/Combobox.tsx b/app/components/Combobox.tsx index 9e8027d6f..e5dacdfd1 100644 --- a/app/components/Combobox.tsx +++ b/app/components/Combobox.tsx @@ -4,7 +4,7 @@ import Fuse from "fuse.js"; import clsx from "clsx"; import type { Unpacked } from "~/utils/types"; import type { GearType, UserWithPlusTier } from "~/db/types"; -import { useUsers } from "~/hooks/swr"; +import { useAllEventsWithMapPools, useUsers } from "~/hooks/swr"; import { useTranslation } from "react-i18next"; import { clothesGearIds, @@ -15,6 +15,7 @@ import { } from "~/modules/in-game-lists"; import { gearImageUrl, mainWeaponImageUrl } from "~/utils/urls"; import { Image } from "./Image"; +import { type SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; const MAX_RESULTS_SHOWN = 6; @@ -36,6 +37,7 @@ interface ComboboxProps { initialValue?: ComboboxOption; clearsInputOnFocus?: boolean; onChange?: (selectedOption?: ComboboxOption) => void; + fullWidth?: boolean; } export function Combobox>({ @@ -49,11 +51,16 @@ export function Combobox>({ className, id, isLoading = false, + fullWidth = false, }: ComboboxProps) { - const [selectedOption, setSelectedOption] = - React.useState>(); - const [lastSelectedOption, setLastSelectedOption] = - React.useState>(); + const { t } = useTranslation(); + + const [selectedOption, setSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); + const [lastSelectedOption, setLastSelectedOption] = React.useState< + Unpacked | undefined + >(initialValue); const [query, setQuery] = React.useState(""); React.useEffect(() => { @@ -75,73 +82,85 @@ export function Combobox>({ const noMatches = filteredOptions.length === 0; + const displayValue = (option: Unpacked) => { + return option?.label ?? ""; + }; + return ( - { - onChange?.(selected); - setSelectedOption(selected); - setLastSelectedOption(selected); - }} - name={inputName} - disabled={isLoading} - > - { - if (clearsInputOnFocus) { - setSelectedOption(undefined); - } +
+ { + onChange?.(selected); + setSelectedOption(selected); + setLastSelectedOption(selected); }} - onBlur={() => { - if (!selectedOption && clearsInputOnFocus) { - setSelectedOption(lastSelectedOption); - } - }} - onChange={(event) => setQuery(event.target.value)} - placeholder={isLoading ? "Loading..." : placeholder} - className={clsx("combobox-input", className)} - displayValue={(option) => - (option as unknown as Unpacked)?.label ?? "" - } - data-cy={`${inputName}-combobox-input`} - id={id} - required={required} - /> - - {noMatches ? ( -
- No matches found 🤔 -
- ) : ( - filteredOptions.map((option) => ( - - {({ active }) => ( -
  • - {option.imgPath && ( - - )} - {option.label} -
  • - )} -
    - )) - )} -
    -
    + { + if (clearsInputOnFocus) { + setSelectedOption(undefined); + } + }} + onBlur={() => { + if (!selectedOption && clearsInputOnFocus) { + setSelectedOption(lastSelectedOption); + } + }} + onChange={(event) => setQuery(event.target.value)} + placeholder={isLoading ? t("actions.loading") : placeholder} + className={clsx("combobox-input", className, { + fullWidth, + })} + // To make SSR prefill work in an uncontrolled component + defaultValue={initialValue ? displayValue(initialValue) : undefined} + displayValue={displayValue} + data-cy={`${inputName}-combobox-input`} + id={id} + required={required} + /> + + {isLoading ? ( +
    {t("actions.loading")}
    + ) : noMatches ? ( +
    + {t("forms.errors.noSearchMatches")}{" "} + 🤔 +
    + ) : ( + filteredOptions.map((option) => ( + + {({ active }) => ( +
  • + {option.imgPath && ( + + )} + {option.label} +
  • + )} +
    + )) + )} +
    + +
    ); } @@ -157,6 +176,7 @@ export function UserCombobox({ ComboboxProps>, "inputName" | "onChange" | "className" | "id" | "required" > & { userIdsToOmit?: Set; initialUserId?: number }) { + const { t } = useTranslation(); const { users, isLoading, isError } = useUsers(); const options = React.useMemo(() => { @@ -181,9 +201,7 @@ export function UserCombobox({ if (isError) { return ( -
    - Something went wrong. Try reloading the page. -
    +
    {t("errors.genericReload")}
    ); } @@ -191,7 +209,7 @@ export function UserCombobox({ ); } + +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) => 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: parseInt(e.value, 10), + name: e.label, + serializedMapPool: e.serializedMapPool, + } + ); + }} + className={className} + id={id} + required={required} + isLoading={isLoading} + fullWidth + /> + ); +} diff --git a/app/components/MapPoolSelector.tsx b/app/components/MapPoolSelector.tsx index 9fe4d955f..d02d340a1 100644 --- a/app/components/MapPoolSelector.tsx +++ b/app/components/MapPoolSelector.tsx @@ -15,6 +15,10 @@ import { CrossIcon } from "./icons/Cross"; import { ArrowLongLeftIcon } from "./icons/ArrowLongLeft"; import * as React from "react"; import type { CalendarEvent } from "~/db/types"; +import type { SerializedMapPoolEvent } from "~/routes/calendar/map-pool-events"; +import { assertType } from "~/utils/types"; +import { MapPoolEventsCombobox } from "./Combobox"; +import { useOnce } from "~/hooks/useOnce"; export type MapPoolSelectorProps = { mapPool: MapPool; @@ -24,9 +28,8 @@ export type MapPoolSelectorProps = { event?: Pick ) => void; className?: string; - recentEvents?: Array< - Pick & { serializedMapPool: string } - >; + recentEvents?: SerializedMapPoolEvent[]; + initialEvent?: Pick; }; export function MapPoolSelector({ @@ -35,11 +38,20 @@ export function MapPoolSelector({ handleRemoval, className, recentEvents, + initialEvent, }: MapPoolSelectorProps) { const { t } = useTranslation(); const [template, setTemplate] = React.useState( - detectTemplate(mapPool) + initialEvent ? "event" : detectTemplate(mapPool) + ); + + const initialSerializedEvent: SerializedMapPoolEvent | undefined = useOnce( + () => + initialEvent && { + ...initialEvent, + serializedMapPool: mapPool.serialized, + } ); const handleStageModesChange = (newMapPool: MapPool) => { @@ -54,7 +66,8 @@ export function MapPoolSelector({ const handleTemplateChange = (template: MapPoolTemplateValue) => { setTemplate(template); - if (template === "none") { + + if (template === "none" || template === "event") { return; } @@ -75,6 +88,8 @@ export function MapPoolSelector({ } return; } + + assertType(); }; return ( @@ -101,6 +116,12 @@ export function MapPoolSelector({ handleChange={handleTemplateChange} recentEvents={recentEvents} /> + {template === "event" && ( + + )} + {(["ANARCHY", "ALL"] as const).map((presetId) => (