Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 18:08:10 +00:00
co-authored by renee-png
parent dd992975c9
commit c0b978dc79
+51 -16
View File
@@ -48,16 +48,27 @@ interface ArchiveRow {
created_at: string;
}
const PAGE_SIZE = 200;
function ArchivePage() {
const { isAdmin } = useAuth();
const [categories, setCategories] = useState<Category[]>([]);
const [activeId, setActiveId] = useState<string>("");
const [rows, setRows] = useState<ArchiveRow[]>([]);
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");
};