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:
@@ -19,7 +19,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { api, type ManagedChannel, type Tag } from "../lib/api";
|
||||
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
|
||||
import { accountKey, LS } from "../lib/storage";
|
||||
import { accountKey, LS, readAccount, writeAccount } from "../lib/storage";
|
||||
import { useDismiss } from "../lib/useDismiss";
|
||||
import { useScrollFade } from "../lib/useScrollFade";
|
||||
import { formatEta, formatTotalHours, relativeTime } from "../lib/format";
|
||||
@@ -30,6 +30,7 @@ import DataTable, { type Column } from "./DataTable";
|
||||
import ChannelLayoutGrid from "./ChannelLayoutGrid";
|
||||
import ViewSwitcher, { type ViewOption } from "./ViewSwitcher";
|
||||
import ColumnSortControl from "./ColumnSortControl";
|
||||
import Pager from "./Pager";
|
||||
import { CHANNEL_LAYOUTS, type ChannelLayout } from "../lib/channelLayout";
|
||||
import { sortRows } from "../lib/columnSort";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
@@ -60,6 +61,10 @@ const LAYOUT_ICON: Record<ChannelLayout, typeof Table> = {
|
||||
cards: LayoutGrid,
|
||||
};
|
||||
|
||||
// Cards-per-page options (mirrors the DataTable default); the card grid gets a pager like the table.
|
||||
const CARD_PAGE_SIZES = [10, 25, 50, 100];
|
||||
const CARD_SIZE_DEFAULT = 25;
|
||||
|
||||
export default function Channels() {
|
||||
const me = useMe();
|
||||
const canWrite = me.can_write;
|
||||
@@ -210,6 +215,10 @@ export default function Channels() {
|
||||
// Channel-name search (from the shared top SearchBar, via props) + tag-chip filtering, applied
|
||||
// client-side over the status-filtered list.
|
||||
const [tagFilter, setTagFilter] = useState<number[]>([]);
|
||||
// Card layout pagination (the table keeps its own inside DataTable). Size persists per account;
|
||||
// the page is transient.
|
||||
const [cardSize, setCardSize] = useState(() => readAccount<number>(LS.channelCardSize, CARD_SIZE_DEFAULT));
|
||||
const [cardPage, setCardPage] = useState(0);
|
||||
// A focus-channel intent (header "without full history" / tag manager) seeds the search box.
|
||||
useEffect(() => {
|
||||
if (focusChannelName) setSearch(focusChannelName);
|
||||
@@ -513,6 +522,30 @@ export default function Channels() {
|
||||
descLabel={t("channels.sort.desc")}
|
||||
/>
|
||||
);
|
||||
// The card layout paginates like the table (a page of cards + a pager), rather than one long
|
||||
// infinite scroll. Slice the sorted rows; the Pager reports page/size changes.
|
||||
const sortedChannels = sortRows(visibleChannels, columns, sort);
|
||||
const cardTotalPages = cardSize <= 0 ? 1 : Math.max(1, Math.ceil(sortedChannels.length / cardSize));
|
||||
const cardSafePage = Math.min(cardPage, cardTotalPages - 1);
|
||||
const pagedCards =
|
||||
cardSize <= 0
|
||||
? sortedChannels
|
||||
: sortedChannels.slice(cardSafePage * cardSize, cardSafePage * cardSize + cardSize);
|
||||
const cardPager = (
|
||||
<Pager
|
||||
page={cardSafePage}
|
||||
size={cardSize}
|
||||
total={sortedChannels.length}
|
||||
pageSizeOptions={CARD_PAGE_SIZES}
|
||||
onPageChange={setCardPage}
|
||||
onSizeChange={(s) => {
|
||||
setCardSize(s);
|
||||
writeAccount(LS.channelCardSize, s);
|
||||
setCardPage(0);
|
||||
}}
|
||||
sizeLabel={t("channels.cardsPerPage")}
|
||||
/>
|
||||
);
|
||||
|
||||
const tabs = (
|
||||
<div className="px-4 pt-4 max-w-[96rem] mx-auto flex items-center justify-between gap-4 flex-wrap">
|
||||
@@ -596,10 +629,13 @@ export default function Channels() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* In table mode the status chips ride in DataTable's own in-band controls row (beside the
|
||||
pager). The card/row layouts have no such row, so surface the same chips here. */}
|
||||
{view === "subscribed" && layout !== "table" && (
|
||||
<div className="px-4 pb-2 max-w-[96rem] mx-auto">{statusChips}</div>
|
||||
{/* In table mode the status chips + pager ride in DataTable's own in-band controls row. The card
|
||||
layout has no such row, so surface the same status chips (left) and its pager (right) here. */}
|
||||
{view === "subscribed" && layout === "cards" && (
|
||||
<div className="px-4 pb-2 max-w-[96rem] mx-auto flex items-center justify-between gap-3 flex-wrap">
|
||||
{statusChips}
|
||||
{cardPager}
|
||||
</div>
|
||||
)}
|
||||
</PageToolbar>
|
||||
{tagManagerOpen && (
|
||||
@@ -675,7 +711,7 @@ export default function Channels() {
|
||||
/>
|
||||
) : (
|
||||
<ChannelLayoutGrid
|
||||
rows={sortRows(visibleChannels, columns, sort)}
|
||||
rows={pagedCards}
|
||||
columns={columns}
|
||||
rowKey={(c) => c.id}
|
||||
leadKey="priority"
|
||||
|
||||
Reference in New Issue
Block a user