Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions src/components/admin/admin-cards-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { CardBadges } from "@/components/card-badges";
import { ColumnFilterCombobox } from "@/components/column-filter-combobox";
import { PaginationControls } from "@/components/data-table/pagination-controls";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Table,
TableBody,
Expand All @@ -39,7 +38,8 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import { AdminFeedback } from "@/components/admin/admin-feedback";
import { Plus, Pencil, Trash2, Search } from "lucide-react";
import { Plus, Pencil, Trash2 } from "lucide-react";
import { CollectionSearchInput } from "@/components/collection-search-input";
import { useTranslations } from "@/i18n/client";
import { useCollectionUrlFilters } from "@/hooks/use-collection-url-filters";
import { useCardBadgeLabels } from "@/hooks/use-card-badge-labels";
Expand Down Expand Up @@ -242,18 +242,14 @@ export function AdminCardsSection({
onSuccessDismiss={() => setSuccess(null)}
/>

<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t("admin.cards.search")}
value={filters.search}
onChange={(e) =>
updateFilters({ search: e.target.value, page: 1 })
}
className="pl-9"
aria-label={t("admin.cards.search")}
/>
</div>
<CollectionSearchInput
urlValue={filters.search}
onSearch={(value) =>
applyAdminFilters({ search: value }, { immediate: true })
}
label={t("admin.cards.search")}
placeholder={t("admin.cards.search")}
/>

<div className="grid gap-2 md:hidden">
<ColumnFilterCombobox
Expand Down
61 changes: 3 additions & 58 deletions src/components/card-table.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState, useMemo, useEffect, useId, useRef } from "react";
import { useState, useMemo, useEffect, useId } from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
Expand All @@ -25,7 +25,7 @@ import { SortableTableHead } from "@/components/data-table/sortable-table-head";
import { PaginationControls } from "@/components/data-table/pagination-controls";
import { ColumnFilterCombobox } from "@/components/column-filter-combobox";
import { FilterChipButton } from "@/components/filter-chip-button";
import { SearchField } from "@/components/search-field";
import { CollectionSearchInput } from "@/components/collection-search-input";
import { useCardBadgeLabels } from "@/hooks/use-card-badge-labels";
import { useCollectionUrlFilters } from "@/hooks/use-collection-url-filters";
import {
Expand All @@ -37,61 +37,6 @@ import {
} from "@/components/ui/table";
import { useTranslations } from "@/i18n/client";

/**
* Isolated search input — its own useState + debounce so that
* every keystroke only re-renders THIS tiny component, not the
* entire CardTable (heavy: TanStack table, comboboxes, etc.).
*
* External URL changes (e.g. "reset filters" link) are detected via
* the getDerivedStateFromProps pattern (calling setState during render),
* which is React's official approach for syncing state from props without
* using effects. The `sentValue` state prevents re-syncing our own URL updates.
*/
function SearchInput({
urlValue,
onSearch,
label,
placeholder,
}: {
urlValue: string;
onSearch: (value: string) => void;
label: string;
placeholder: string;
}) {
const [inputValue, setInputValue] = useState(urlValue);
const [prevUrlValue, setPrevUrlValue] = useState(urlValue);
// Tracks the value we last sent via onSearch — distinguishes our own
// URL updates from external navigation/resets.
const [sentValue, setSentValue] = useState(urlValue);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

// getDerivedStateFromProps: sync input when URL changes externally.
if (prevUrlValue !== urlValue) {
setPrevUrlValue(urlValue);
if (sentValue !== urlValue) {
setInputValue(urlValue);
}
}

function handleChange(newValue: string) {
setInputValue(newValue);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
setSentValue(newValue);
onSearch(newValue);
}, 300);
}

return (
<SearchField
value={inputValue}
onChange={handleChange}
label={label}
placeholder={placeholder}
/>
);
}

const SORT_COLUMN_KEYS = [
"player",
"team",
Expand Down Expand Up @@ -308,7 +253,7 @@ export function CardTable({
return (
<div className="space-y-4">
<div className="flex flex-col gap-4">
<SearchInput
<CollectionSearchInput
urlValue={urlFilters.search}
onSearch={(value) =>
updateFilters({ search: value }, { immediate: true })
Expand Down
53 changes: 53 additions & 0 deletions src/components/collection-search-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

import { useRef, useState } from "react";
import { SearchField } from "@/components/search-field";

/**
* Champ de recherche collection/admin : état local + debounce 300 ms,
* puis sync URL via le callback parent (`immediate: true` recommandé).
*/
export function CollectionSearchInput({
urlValue,
onSearch,
label,
placeholder,
className,
}: {
urlValue: string;
onSearch: (value: string) => void;
label: string;
placeholder: string;
className?: string;
}) {
const [inputValue, setInputValue] = useState(urlValue);
const [prevUrlValue, setPrevUrlValue] = useState(urlValue);
const [sentValue, setSentValue] = useState(urlValue);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

if (prevUrlValue !== urlValue) {
setPrevUrlValue(urlValue);
if (sentValue !== urlValue) {
setInputValue(urlValue);
}
}

function handleChange(newValue: string) {
setInputValue(newValue);
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
setSentValue(newValue);
onSearch(newValue);
}, 300);
}

return (
<SearchField
value={inputValue}
onChange={handleChange}
label={label}
placeholder={placeholder}
className={className}
/>
);
}