diff --git a/src/routes/archive.index.tsx b/src/routes/archive.index.tsx index 5538bf6..7182009 100644 --- a/src/routes/archive.index.tsx +++ b/src/routes/archive.index.tsx @@ -48,16 +48,27 @@ interface ArchiveRow { created_at: string; } +const PAGE_SIZE = 200; + function ArchivePage() { const { isAdmin } = useAuth(); const [categories, setCategories] = useState([]); const [activeId, setActiveId] = useState(""); const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [totalCount, setTotalCount] = useState(0); const [uploadOpen, setUploadOpen] = useState(false); const [newCatOpen, setNewCatOpen] = useState(false); + // Debounce search input (300ms) + useEffect(() => { + const t = setTimeout(() => setDebouncedSearch(search.trim()), 300); + return () => clearTimeout(t); + }, [search]); + const loadCategories = async () => { const { data, error } = await supabase .from("archive_categories") @@ -74,31 +85,59 @@ function ArchivePage() { } }; - const loadRows = async (categoryId: string) => { + const buildQuery = (categoryId: string, searchTerm: string) => { + let q = supabase + .from("archive_records") + .select("*", { count: "exact" }) + .eq("category_id", categoryId); + if (searchTerm) { + // Cast jsonb to text and search case-insensitively + q = q.ilike("data::text", `%${searchTerm}%`); + } + return q.order("created_at", { ascending: false }); + }; + + const loadRows = async (categoryId: string, searchTerm: string) => { if (!categoryId) return; setLoading(true); - const { data, error } = await supabase - .from("archive_records") - .select("*") - .eq("category_id", categoryId) - .order("created_at", { ascending: false }) - .limit(1000); + const { data, error, count } = await buildQuery(categoryId, searchTerm).range( + 0, + PAGE_SIZE - 1, + ); if (error) { toast.error(error.message); setLoading(false); return; } setRows((data ?? []) as ArchiveRow[]); + setTotalCount(count ?? 0); setLoading(false); }; + const loadMore = async () => { + if (!activeId || loadingMore) return; + setLoadingMore(true); + const { data, error } = await buildQuery(activeId, debouncedSearch).range( + rows.length, + rows.length + PAGE_SIZE - 1, + ); + if (error) { + toast.error(error.message); + setLoadingMore(false); + return; + } + setRows((prev) => [...prev, ...((data ?? []) as ArchiveRow[])]); + setLoadingMore(false); + }; + useEffect(() => { loadCategories(); }, []); useEffect(() => { - if (activeId) loadRows(activeId); - }, [activeId]); + if (activeId) loadRows(activeId, debouncedSearch); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeId, debouncedSearch]); const activeCategory = categories.find((c) => c.id === activeId); @@ -113,12 +152,6 @@ function ArchivePage() { return Array.from(set); }, [rows]); - const filteredRows = useMemo(() => { - if (!search.trim()) return rows; - const s = search.toLowerCase(); - return rows.filter((r) => JSON.stringify(r.data).toLowerCase().includes(s)); - }, [rows, search]); - const deleteRow = async (id: string) => { if (!confirm("Delete this record?")) return; const { error } = await supabase.from("archive_records").delete().eq("id", id); @@ -127,18 +160,20 @@ function ArchivePage() { return; } setRows((prev) => prev.filter((r) => r.id !== id)); + setTotalCount((c) => Math.max(0, c - 1)); toast.success("Record deleted"); }; const deleteAllInCategory = async () => { if (!activeCategory) return; - if (!confirm(`Delete ALL ${rows.length} records in "${activeCategory.label}"? This cannot be undone.`)) return; + if (!confirm(`Delete ALL ${totalCount} records in "${activeCategory.label}"? This cannot be undone.`)) return; const { error } = await supabase.from("archive_records").delete().eq("category_id", activeCategory.id); if (error) { toast.error(error.message); return; } setRows([]); + setTotalCount(0); toast.success("All records deleted"); }; @@ -195,7 +230,7 @@ function ArchivePage() { className="flex-1" />
- {filteredRows.length} of {rows.length} rows + Showing {rows.length} of {totalCount} {debouncedSearch ? "matching" : ""} rows
{isAdmin && rows.length > 0 && ( + + )} @@ -278,7 +320,7 @@ function ArchivePage() { category={activeCategory} onDone={() => { setUploadOpen(false); - loadRows(activeCategory.id); + loadRows(activeCategory.id, debouncedSearch); }} /> )}