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:
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
// The Prev / page-input / Next + "N per page" cluster, lifted out of DataTable so a non-table view
|
||||
// (the channel manager's card layout) can paginate the same way. Controlled: the caller owns page +
|
||||
// size and slices its own rows; this only renders the controls and reports changes. `sizeLabel` lets
|
||||
// the caller say "Rows per page" or "Cards per page". Returns null when there's nothing to page.
|
||||
export default function Pager({
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
pageSizeOptions,
|
||||
onPageChange,
|
||||
onSizeChange,
|
||||
sizeLabel,
|
||||
}: {
|
||||
/** Current page (0-based). */
|
||||
page: number;
|
||||
/** Page size; `<= 0` means "All" (one page). */
|
||||
size: number;
|
||||
/** Total item count across all pages. */
|
||||
total: number;
|
||||
pageSizeOptions: number[];
|
||||
onPageChange: (page: number) => void;
|
||||
onSizeChange: (size: number) => void;
|
||||
sizeLabel: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const allRows = size <= 0;
|
||||
const totalPages = allRows ? 1 : Math.max(1, Math.ceil(total / size));
|
||||
const safePage = Math.min(page, totalPages - 1);
|
||||
|
||||
// Local text for the editable "jump to page" box (committed on Enter/blur).
|
||||
const [pageInput, setPageInput] = useState(String(safePage + 1));
|
||||
useEffect(() => setPageInput(String(safePage + 1)), [safePage]);
|
||||
function commitPageInput() {
|
||||
const n = parseInt(pageInput, 10);
|
||||
if (!isNaN(n)) onPageChange(Math.min(totalPages - 1, Math.max(0, n - 1)));
|
||||
else setPageInput(String(safePage + 1));
|
||||
}
|
||||
|
||||
if (!(totalPages > 1 || total > 0)) return null;
|
||||
return (
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onPageChange(Math.max(0, safePage - 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={() => onPageChange(Math.min(totalPages - 1, safePage + 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>
|
||||
)}
|
||||
{total > 0 && (
|
||||
<label className="flex items-center gap-2 text-muted">
|
||||
{sizeLabel}
|
||||
<select
|
||||
value={size}
|
||||
onChange={(e) => onSizeChange(Number(e.target.value))}
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user