feat(managers): paginate the card layout + its top scroll-fade
UAT round 3 feedback on the card layout: - Add a pager + "Cards per page" to the card view (it infinitely-scrolled before). Extracted the pager cluster out of DataTable into a reusable Pager component (the table now renders it too — no behaviour change); the card view slices the sorted rows and drives its own per-account page size (subscribed + discovery). - Restore the top edge-fade in the card layout. The channels page registers fadeTop:false for its sticky-header table; the card layout has no sticky header, so App now re-enables the top fade when the channel layout is cards.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ArrowDown, ArrowUp, ChevronLeft, ChevronRight, ListFilter, X } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, ListFilter, X } from "lucide-react";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { sortRows, cycleSort, type SortState } from "../lib/columnSort";
|
||||
import Pager from "./Pager";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
|
||||
// A reusable, client-side data table: per-column sort + filter (in the header) + pagination,
|
||||
@@ -132,8 +133,6 @@ export default function DataTable<T>({
|
||||
const [page, setPage] = useState(initial.page);
|
||||
const [size, setSize] = useState(initial.size ?? pageSize);
|
||||
const [openFilter, setOpenFilter] = useState<string | null>(null);
|
||||
// Local text for the editable "jump to page" box (committed on Enter/blur).
|
||||
const [pageInput, setPageInput] = useState("1");
|
||||
const popRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -181,13 +180,6 @@ export default function DataTable<T>({
|
||||
const safePage = Math.min(page, totalPages - 1);
|
||||
const paged = allRows ? sorted : sorted.slice(safePage * size, safePage * size + size);
|
||||
|
||||
useEffect(() => setPageInput(String(safePage + 1)), [safePage]);
|
||||
function commitPageInput() {
|
||||
const n = parseInt(pageInput, 10);
|
||||
if (!isNaN(n)) setPage(Math.min(totalPages - 1, Math.max(0, n - 1)));
|
||||
else setPageInput(String(safePage + 1));
|
||||
}
|
||||
|
||||
function toggleSort(key: string) {
|
||||
const next = cycleSort(sort, key);
|
||||
if (controlledSort) onSortChange!(next);
|
||||
@@ -291,66 +283,20 @@ export default function DataTable<T>({
|
||||
// The pager + rows-per-page cluster on its own, so it can be rendered a second time at the bottom
|
||||
// (controlsPosition "both") WITHOUT repeating the leading controls (e.g. the status chips) — those
|
||||
// are header controls and belong only at the top.
|
||||
const pagerNode =
|
||||
totalPages > 1 || sorted.length > 0 ? (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={safePage === 0}
|
||||
className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
{t("datatable.pager.prev")}
|
||||
</button>
|
||||
<span className="text-muted flex items-center gap-1.5">
|
||||
{t("datatable.pager.pageLabel")}
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
value={pageInput}
|
||||
onChange={(e) => setPageInput(e.target.value)}
|
||||
onBlur={commitPageInput}
|
||||
onKeyDown={(e) => e.key === "Enter" && commitPageInput()}
|
||||
aria-label={t("datatable.pager.pageLabel")}
|
||||
className="w-14 bg-card border border-border rounded-md px-1.5 py-1 text-xs text-center tabular-nums outline-none focus:border-accent"
|
||||
/>
|
||||
{t("datatable.pager.ofTotal", { total: totalPages })}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={safePage >= totalPages - 1}
|
||||
className="glass-card glass-hover flex items-center gap-1 px-3 py-1.5 rounded-lg disabled:opacity-40 transition"
|
||||
>
|
||||
{t("datatable.pager.next")}
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{sorted.length > 0 && (
|
||||
<label className="flex items-center gap-2 text-muted">
|
||||
{t("datatable.rowsPerPage")}
|
||||
<select
|
||||
value={size}
|
||||
onChange={(e) => {
|
||||
setSize(Number(e.target.value));
|
||||
setPage(0);
|
||||
}}
|
||||
className="bg-card border border-border rounded-md px-1.5 py-1 text-xs outline-none focus:border-accent"
|
||||
>
|
||||
{pageSizeOptions.map((n) => (
|
||||
<option key={n} value={n}>
|
||||
{n}
|
||||
</option>
|
||||
))}
|
||||
<option value={0}>{t("datatable.all")}</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
const pagerNode = (
|
||||
<Pager
|
||||
page={safePage}
|
||||
size={size}
|
||||
total={sorted.length}
|
||||
pageSizeOptions={pageSizeOptions}
|
||||
onPageChange={setPage}
|
||||
onSizeChange={(s) => {
|
||||
setSize(s);
|
||||
setPage(0);
|
||||
}}
|
||||
sizeLabel={t("datatable.rowsPerPage")}
|
||||
/>
|
||||
);
|
||||
|
||||
const controls =
|
||||
controlsLeading != null || pagerNode ? (
|
||||
|
||||
Reference in New Issue
Block a user