diff --git a/messages/en.json b/messages/en.json index be4efe5..855f8e6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -271,7 +271,11 @@ "slotOptional": "Optional", "player": "Player", "allVariations": "All variations", + "allSets": "All sets", + "filterSet": "Filter by set", "filterVariation": "Variation…", + "setChip": "{set} ({count})", + "setCount": "{count} cards", "count": "{count} entries", "addEntry": "Add an entry", "fieldsRequired": "Set, variation, and player are required.", @@ -279,7 +283,7 @@ "added": "Entry added.", "deleted": "Entry removed.", "actions": "Actions", - "deleteEntry": "Remove {player}" + "deleteEntry": "Remove {player} — {set}" }, "frNba": { "title": "Fr NBA/WNBA", diff --git a/messages/fr.json b/messages/fr.json index 44e9410..5991433 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -271,7 +271,11 @@ "slotOptional": "Optionnel", "player": "Joueur", "allVariations": "Toutes les variations", + "allSets": "Tous les sets", + "filterSet": "Filtrer par set", "filterVariation": "Variation…", + "setChip": "{set} ({count})", + "setCount": "{count} cartes", "count": "{count} entrées", "addEntry": "Ajouter une entrée", "fieldsRequired": "Set, variation et joueur sont obligatoires.", @@ -279,7 +283,7 @@ "added": "Entrée ajoutée.", "deleted": "Entrée supprimée.", "actions": "Actions", - "deleteEntry": "Supprimer {player}" + "deleteEntry": "Supprimer {player} — {set}" }, "frNba": { "title": "Fr NBA/WNBA", diff --git a/src/components/guides/wanted-board.tsx b/src/components/guides/wanted-board.tsx index 339782d..c6ec0c5 100644 --- a/src/components/guides/wanted-board.tsx +++ b/src/components/guides/wanted-board.tsx @@ -1,34 +1,14 @@ "use client"; -import { useCallback, useMemo, useState } from "react"; -import { - useTable, - flexRender, - type ColumnDef, - type Row, - type SortingState, -} from "@tanstack/react-table"; -import { - interactiveTableFeatures, - type InteractiveTableFeatures, -} from "@/components/data-table/table-features"; +import { useCallback, useId, useMemo, useState } from "react"; import { createWantedEntry, deleteWantedEntry } from "@/lib/guides-client"; import type { WantedBlock, WantedEntry } from "@/lib/types"; import { ColumnFilterCombobox } from "@/components/column-filter-combobox"; -import { SortableTableHead } from "@/components/data-table/sortable-table-head"; -import { TablePagination } from "@/components/data-table/table-pagination"; +import { FilterChipButton } from "@/components/filter-chip-button"; import { SearchField } from "@/components/search-field"; -import { useStableTablePagination } from "@/hooks/use-stable-table-pagination"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { - Table, - TableBody, - TableCell, - TableHeader, - TableRow, -} from "@/components/ui/table"; import { Plus, Trash2 } from "lucide-react"; import { useTranslations } from "@/i18n/client"; @@ -38,15 +18,26 @@ interface WantedBoardProps { type WantedRow = WantedEntry & { set: string }; +type WantedSetGroup = { + set: string; + entries: WantedRow[]; +}; + +function compareWantedRows(a: WantedRow, b: WantedRow): number { + const variationCmp = a.variation.localeCompare(b.variation); + if (variationCmp !== 0) return variationCmp; + const slotA = a.slot ?? Number.POSITIVE_INFINITY; + const slotB = b.slot ?? Number.POSITIVE_INFINITY; + if (slotA !== slotB) return slotA - slotB; + return a.player.localeCompare(b.player); +} + export function WantedBoard({ initialBlocks }: WantedBoardProps) { const t = useTranslations(); const [blocks, setBlocks] = useState(initialBlocks); const [search, setSearch] = useState(""); + const [setFilter, setSetFilter] = useState(""); const [variationFilter, setVariationFilter] = useState(""); - const [sorting, setSorting] = useState([ - { id: "variation", desc: false }, - { id: "slot", desc: false }, - ]); const [setName, setSetName] = useState(""); const [variation, setVariation] = useState(""); const [slot, setSlot] = useState(""); @@ -64,22 +55,35 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { ); const setNames = useMemo( - () => [...new Set(blocks.map((block) => block.set))].sort((a, b) => - a.localeCompare(b) - ), - [blocks] - ); - - const variations = useMemo( () => - [...new Set(rows.map((row) => row.variation))].sort((a, b) => + [...new Set(blocks.map((block) => block.set))].sort((a, b) => a.localeCompare(b) ), - [rows] + [blocks] ); + const setCounts = useMemo(() => { + const counts = new Map(); + for (const row of rows) { + counts.set(row.set, (counts.get(row.set) ?? 0) + 1); + } + return counts; + }, [rows]); + + const variations = useMemo(() => { + const source = setFilter + ? rows.filter((row) => row.set === setFilter) + : rows; + return [...new Set(source.map((row) => row.variation))].sort((a, b) => + a.localeCompare(b) + ); + }, [rows, setFilter]); + const filteredRows = useMemo(() => { let result = rows; + if (setFilter) { + result = result.filter((row) => row.set === setFilter); + } if (variationFilter) { result = result.filter((row) => row.variation === variationFilter); } @@ -93,7 +97,26 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { ); } return result; - }, [rows, search, variationFilter]); + }, [rows, search, setFilter, variationFilter]); + + const groupedSets = useMemo((): WantedSetGroup[] => { + const bySet = new Map(); + for (const row of filteredRows) { + const list = bySet.get(row.set); + if (list) { + list.push(row); + } else { + bySet.set(row.set, [row]); + } + } + + return [...bySet.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([set, entries]) => ({ + set, + entries: [...entries].sort(compareWantedRows), + })); + }, [filteredRows]); async function handleAdd() { const trimmedSet = setName.trim(); @@ -115,13 +138,13 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { setSuccess(null); setLoading(true); try { - const blocks = await createWantedEntry({ + const nextBlocks = await createWantedEntry({ set: trimmedSet, variation: trimmedVariation, player: trimmedPlayer, slot: slotValue, }); - setBlocks(blocks); + setBlocks(nextBlocks); setVariation(""); setSlot(""); setPlayer(""); @@ -138,8 +161,8 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { setSuccess(null); setLoading(true); try { - const blocks = await deleteWantedEntry(id); - setBlocks(blocks); + const nextBlocks = await deleteWantedEntry(id); + setBlocks(nextBlocks); setSuccess(t("guides.wanted.deleted")); } catch { setFormError(t("errors.updateFailed")); @@ -150,86 +173,6 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { [t] ); - const columns = useMemo[]>( - () => [ - { - accessorKey: "set", - header: t("guides.wanted.set"), - cell: ({ row }) => ( - {row.original.set} - ), - }, - { - accessorKey: "variation", - header: t("guides.wanted.variation"), - cell: ({ row }) => ( - - {row.original.variation} - - ), - }, - { - accessorKey: "slot", - header: t("guides.wanted.slot"), - cell: ({ row }) => ( - - {row.original.slot ?? "—"} - - ), - sortingFn: ( - a: Row, - b: Row - ) => (a.original.slot ?? 999) - (b.original.slot ?? 999), - }, - { - accessorKey: "player", - header: t("guides.wanted.player"), - cell: ({ row }) => ( - {row.original.player} - ), - }, - { - id: "actions", - header: t("guides.wanted.actions"), - enableSorting: false, - cell: ({ row }) => ( - - ), - }, - ], - [handleDelete, loading, t] - ); - - const filterResetKey = `${search}\0${variationFilter}`; - - const tablePagination = useStableTablePagination({ - pageSize: 25, - resetKey: filterResetKey, - rowCount: filteredRows.length, - }); - - const table = useTable({ - features: interactiveTableFeatures, - data: filteredRows, - columns, - state: { sorting, pagination: tablePagination.pagination }, - onSortingChange: setSorting, - onPaginationChange: tablePagination.onPaginationChange, - autoResetPageIndex: tablePagination.autoResetPageIndex, - }); - return (
@@ -292,7 +235,9 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) { {t("common.add")} {success ? ( -

{success}

+

+ {success} +

) : null} {formError ? (

@@ -302,14 +247,35 @@ export function WantedBoard({ initialBlocks }: WantedBoardProps) {

- {blocks.map((block) => ( -

0 ? ( +

- {block.set} -

- ))} + { + setSetFilter(""); + }} + /> + {setNames.map((name) => ( + { + setSetFilter(pressed ? name : ""); + setVariationFilter(""); + }} + /> + ))} +
+ ) : null}
{t("guides.wanted.count", { count: filteredRows.length })} - {filteredRows.length !== rows.length && - ` ${t("common.filteredOf", { total: rows.length })}`} + {filteredRows.length !== rows.length + ? ` ${t("common.filteredOf", { total: rows.length })}` + : null}

-
- {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( -
-
-

{row.original.player}

-

- {row.original.set} · {row.original.variation} - {row.original.slot != null ? ` · #${row.original.slot}` : ""} + {groupedSets.length === 0 ? ( +

+ {t("common.noneFound")} +
+ ) : ( +
+ {groupedSets.map((group) => ( + + ))} +
+ )} +
+ ); +} + +function WantedSetSection({ + set, + entries, + loading, + onDelete, +}: { + set: string; + entries: WantedRow[]; + loading: boolean; + onDelete: (id: number) => void | Promise; +}) { + const t = useTranslations(); + const headingId = useId(); + + return ( +
+
+

+ {set} +

+

+ {t("guides.wanted.setCount", { count: entries.length })} +

+
+
    + {entries.map((entry) => ( +
  • +
    +
    +

    + {entry.player} +

    +

    + {entry.variation} + {entry.slot != null ? ( + + {` · #${entry.slot}`} + + ) : null}

    -
- )) - ) : ( -
- {t("common.noneFound")} -
- )} -
- -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - {t("common.noneFound")} - - - )} - -
-
- - -
+ + + ))} + + ); }