Added paginated archives

X-Lovable-Edit-ID: edt-9764f8ae-12fd-4626-8390-18cee99190ae
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:49 +00:00
co-authored by renee-png
+61 -19
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");
};
@@ -195,7 +230,7 @@ function ArchivePage() {
className="flex-1"
/>
<div className="text-xs text-muted-foreground whitespace-nowrap">
{filteredRows.length} of {rows.length} rows
Showing {rows.length} of {totalCount} {debouncedSearch ? "matching" : ""} rows
</div>
{isAdmin && rows.length > 0 && (
<Button variant="outline" size="sm" onClick={deleteAllInCategory}>
@@ -233,7 +268,7 @@ function ArchivePage() {
</tr>
</thead>
<tbody>
{filteredRows.map((r) => (
{rows.map((r) => (
<tr key={r.id} className="border-t hover:bg-muted/30">
{columns.map((c) => (
<td key={c} className="px-3 py-2 align-top max-w-xs truncate" title={String(r.data?.[c] ?? "")}>
@@ -268,6 +303,13 @@ function ArchivePage() {
</table>
</div>
)}
{!loading && rows.length > 0 && rows.length < totalCount && (
<div className="border-t p-3 text-center">
<Button variant="outline" size="sm" onClick={loadMore} disabled={loadingMore}>
{loadingMore ? "Loading…" : `Load ${Math.min(PAGE_SIZE, totalCount - rows.length)} more`}
</Button>
</div>
)}
</CardContent>
</Card>
@@ -278,7 +320,7 @@ function ArchivePage() {
category={activeCategory}
onDone={() => {
setUploadOpen(false);
loadRows(activeCategory.id);
loadRows(activeCategory.id, debouncedSearch);
}}
/>
)}