From 8a3de998dcb3d7c58998e8dff22e6b00d3ff7862 Mon Sep 17 00:00:00 2001 From: Duard Harmse Date: Wed, 7 Jan 2026 17:27:13 +0200 Subject: [PATCH 1/2] Add search box to CardSelect --- src/components/common/CardDrawer.tsx | 1 + src/components/common/CardSelect.tsx | 98 +++++++++++++++++++--------- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/components/common/CardDrawer.tsx b/src/components/common/CardDrawer.tsx index 51178b12..72851199 100644 --- a/src/components/common/CardDrawer.tsx +++ b/src/components/common/CardDrawer.tsx @@ -78,6 +78,7 @@ const CardDrawer = ({ onSelect={onSelectCard} canRemove={!!selectedCard} onRemove={onRemoveCard} + open={open} /> diff --git a/src/components/common/CardSelect.tsx b/src/components/common/CardSelect.tsx index 146005f8..bcfc15ec 100644 --- a/src/components/common/CardSelect.tsx +++ b/src/components/common/CardSelect.tsx @@ -1,8 +1,9 @@ import * as _ from "lodash-es"; -import { useEffect } from "react"; +import { useEffect, useRef, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import EditIcon from "@mui/icons-material/Edit"; +import ClearIcon from "@mui/icons-material/Clear"; import { Button, IconButton, @@ -11,6 +12,7 @@ import { ListSubheader, Stack, Typography, + Input, } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; @@ -91,6 +93,7 @@ interface CardSelectProps { onSelect?: (value: TCard) => void; canRemove?: boolean; onRemove?: () => void; + open?: boolean; } const CardSelect = ({ @@ -105,13 +108,20 @@ const CardSelect = ({ onSelect, canRemove = false, onRemove, + open, }: CardSelectProps) => { const intl = useIntl(); + const [search, setSearch] = useState(''); + const cardNameOptions = getOptions( cards, (c) => c.name, (n) => getLocalizedCardName(intl, n) ?? n, + ).filter(card => + getLocalizedCardName(intl, card.name)! + .toLowerCase() + .includes(search.toLowerCase()) ); const cardNameOptionsByType = _.groupBy( @@ -190,6 +200,7 @@ const CardSelect = ({ }; const handleSelectCardName = (value: TCard) => { + clearSearch(); handleSelect(value.name); }; @@ -216,6 +227,18 @@ const CardSelect = ({ onGameBoxChange(undefined); }; + const searchRef = useRef(null); + + const clearSearch = () => { + setSearch('') + } + + useEffect(() => { + if (open) { + searchRef.current?.querySelector('input')!.focus(); + } + }, [searchRef, open]) + return ( @@ -239,36 +262,49 @@ const CardSelect = ({ ) : ( - ({ - "--List-padding": 0, - "--ListItem-paddingX": 0, - "--ListItem-stickyTop": "-1px", - "--ListItem-stickyBackground": theme.palette.background.surface, - "flex": "1 1", - "overflowY": "auto", - })} - > - {sortedCardTypes.map((type) => ( - - {Object.keys(cardNameOptionsByType).length > 1 && ( - - {intl.formatMessage(CardTypeMessages[type].plural)} - - )} - {cardNameOptionsByType[type].map((card) => ( - - handleSelectCardName(card)} - /> - - ))} - - ))} - + + + + + } + onChange={(event) => setSearch(event.target.value)} + value={search} + /> + + ({ + "--List-padding": 0, + "--ListItem-paddingX": 0, + "--ListItem-stickyTop": "-1px", + "--ListItem-stickyBackground": theme.palette.background.surface, + "flex": "1 1", + "overflowY": "auto", + })} + > + {sortedCardTypes.map((type) => ( + + {Object.keys(cardNameOptionsByType).length > 1 && ( + + {intl.formatMessage(CardTypeMessages[type].plural)} + + )} + {cardNameOptionsByType[type].map((card) => ( + + handleSelectCardName(card)} + /> + + ))} + + ))} + + )} {canSelectTreeSymbol && ( From f3f0bdc1d8b76bc0be681fdbe8e4a0d8215473b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Oldag?= Date: Thu, 2 Apr 2026 17:52:10 +0200 Subject: [PATCH 2/2] Minor improvements to card search * Use autoFocus prop instead of ref * Display subheaders in search results * Show no results placeholder * Add icon and placeholder to input --- src/components/common/CardDrawer.tsx | 31 +++++----- src/components/common/CardSelect.tsx | 89 +++++++++++++++++----------- src/translations/de.json | 2 + src/translations/en.json | 2 + src/translations/nl.json | 2 + src/translations/pt-br.json | 2 + 6 files changed, 79 insertions(+), 49 deletions(-) diff --git a/src/components/common/CardDrawer.tsx b/src/components/common/CardDrawer.tsx index 72851199..2598c64d 100644 --- a/src/components/common/CardDrawer.tsx +++ b/src/components/common/CardDrawer.tsx @@ -47,7 +47,7 @@ const CardDrawer = ({ open={open} onClose={onClose} sx={{ - "--Drawer-verticalSize": "min(571px, 65%)", + "--Drawer-verticalSize": "100%", }} > @@ -66,20 +66,21 @@ const CardDrawer = ({ )} - + {open && ( + + )} ); diff --git a/src/components/common/CardSelect.tsx b/src/components/common/CardSelect.tsx index bcfc15ec..1ae404b3 100644 --- a/src/components/common/CardSelect.tsx +++ b/src/components/common/CardSelect.tsx @@ -1,18 +1,19 @@ import * as _ from "lodash-es"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import EditIcon from "@mui/icons-material/Edit"; import ClearIcon from "@mui/icons-material/Clear"; +import EditIcon from "@mui/icons-material/Edit"; +import SearchIcon from "@mui/icons-material/Search"; import { Button, IconButton, + Input, List, ListItem, ListSubheader, Stack, Typography, - Input, } from "@mui/joy"; import { SxProps } from "@mui/joy/styles/types"; @@ -93,7 +94,6 @@ interface CardSelectProps { onSelect?: (value: TCard) => void; canRemove?: boolean; onRemove?: () => void; - open?: boolean; } const CardSelect = ({ @@ -108,20 +108,21 @@ const CardSelect = ({ onSelect, canRemove = false, onRemove, - open, }: CardSelectProps) => { const intl = useIntl(); - const [search, setSearch] = useState(''); + const [search, setSearch] = useState(""); + + const matchingCards = cards.filter((card) => + getLocalizedCardName(intl, card.name) + ?.toLowerCase() + ?.includes(search.toLowerCase()), + ); const cardNameOptions = getOptions( - cards, + matchingCards, (c) => c.name, (n) => getLocalizedCardName(intl, n) ?? n, - ).filter(card => - getLocalizedCardName(intl, card.name)! - .toLowerCase() - .includes(search.toLowerCase()) ); const cardNameOptionsByType = _.groupBy( @@ -200,7 +201,7 @@ const CardSelect = ({ }; const handleSelectCardName = (value: TCard) => { - clearSearch(); + handleClearSearch(); handleSelect(value.name); }; @@ -227,17 +228,16 @@ const CardSelect = ({ onGameBoxChange(undefined); }; - const searchRef = useRef(null); - - const clearSearch = () => { - setSearch('') - } + const handleClearSearch = () => { + setSearch(""); + }; - useEffect(() => { - if (open) { - searchRef.current?.querySelector('input')!.focus(); - } - }, [searchRef, open]) + const showSubheaders = + new Set( + cards + .flatMap((c) => c.types) + .filter((t) => !EXPANSION_CARD_TYPES.includes(t)), + ).size > 1; return ( @@ -262,17 +262,38 @@ const CardSelect = ({ ) : ( - - - - - } - onChange={(event) => setSearch(event.target.value)} - value={search} - /> + + + {([placeholder]) => ( + } + endDecorator={ + search.length > 0 && ( + + + + ) + } + placeholder={placeholder as string} + onChange={(event) => setSearch(event.target.value)} + value={search} + /> + )} + + + {search.length > 0 && cardNameOptions.length === 0 && ( + + + + )} ({ @@ -286,7 +307,7 @@ const CardSelect = ({ > {sortedCardTypes.map((type) => ( - {Object.keys(cardNameOptionsByType).length > 1 && ( + {showSubheaders && ( {intl.formatMessage(CardTypeMessages[type].plural)} diff --git a/src/translations/de.json b/src/translations/de.json index 6199b591..c4e8fcee 100644 --- a/src/translations/de.json +++ b/src/translations/de.json @@ -161,6 +161,8 @@ "CardSelect.header.expansion": "Erweiterung", "CardSelect.header.treeSymbol": "Baumsymbol", "CardSelect.remove": "Karte entfernen", + "CardSelect.search.noResults": "Keine Karten mit diesem Namen gefunden.", + "CardSelect.search.placeholder": "Suche...", "CardTypes.Alps.plural": "Alpen", "CardTypes.Alps.singular": "Alpen", "CardTypes.Amphibian.plural": "Amphibien", diff --git a/src/translations/en.json b/src/translations/en.json index 036ee3aa..c298bcc5 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -161,6 +161,8 @@ "CardSelect.header.expansion": "Expansion", "CardSelect.header.treeSymbol": "Tree symbol", "CardSelect.remove": "Remove card", + "CardSelect.search.noResults": "No cards found with this name.", + "CardSelect.search.placeholder": "Search...", "CardTypes.Alps.plural": "Alps", "CardTypes.Alps.singular": "Alps", "CardTypes.Amphibian.plural": "Amphibians", diff --git a/src/translations/nl.json b/src/translations/nl.json index 4b0630ef..c453af18 100644 --- a/src/translations/nl.json +++ b/src/translations/nl.json @@ -161,6 +161,8 @@ "CardSelect.header.expansion": "Uitbreiding", "CardSelect.header.treeSymbol": "Boom Symbool", "CardSelect.remove": "Kaart verwijderen", + "CardSelect.search.noResults": "Geen kaarten gevonden met deze naam.", + "CardSelect.search.placeholder": "Zoeken...", "CardTypes.Alps.plural": "Alpen", "CardTypes.Alps.singular": "Alp", "CardTypes.Amphibian.plural": "Amfibieën", diff --git a/src/translations/pt-br.json b/src/translations/pt-br.json index b32f4ad8..38b853c9 100644 --- a/src/translations/pt-br.json +++ b/src/translations/pt-br.json @@ -161,6 +161,8 @@ "CardSelect.header.expansion": "Expansão", "CardSelect.header.treeSymbol": "Símbolo de árvore", "CardSelect.remove": "Remover carta", + "CardSelect.search.noResults": "Nenhuma carta encontrada com esse nome.", + "CardSelect.search.placeholder": "Pesquisar...", "CardTypes.Alps.plural": "Alpes", "CardTypes.Alps.singular": "Alpes", "CardTypes.Amphibian.plural": "Anfíbios",