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:
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user