From e413768cc08714c76b1546fb089384d6366b95dd Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 31 Jan 2026 10:37:05 +0900 Subject: feat(cards): add pagination to deck cards page (50 cards per page) Co-Authored-By: Claude Opus 4.5 --- src/client/pages/DeckCardsPage.tsx | 120 +++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 12 deletions(-) (limited to 'src/client') diff --git a/src/client/pages/DeckCardsPage.tsx b/src/client/pages/DeckCardsPage.tsx index 416760a..8ac5c91 100644 --- a/src/client/pages/DeckCardsPage.tsx +++ b/src/client/pages/DeckCardsPage.tsx @@ -1,5 +1,6 @@ import { faChevronLeft, + faChevronRight, faFile, faFileImport, faLayerGroup, @@ -9,7 +10,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useAtomValue, useSetAtom } from "jotai"; -import { Suspense, useMemo, useState, useTransition } from "react"; +import { Suspense, useCallback, useMemo, useState, useTransition } from "react"; import { Link, useParams } from "wouter"; import { type Card, cardsByDeckAtomFamily, deckByIdAtomFamily } from "../atoms"; import { CreateNoteModal } from "../components/CreateNoteModal"; @@ -24,6 +25,8 @@ import { LoadingSpinner } from "../components/LoadingSpinner"; /** Combined type for display: note group */ type CardDisplayItem = { type: "note"; noteId: string; cards: Card[] }; +const CARDS_PER_PAGE = 50; + const CardStateLabels: Record = { 0: "New", 1: "Learning", @@ -171,6 +174,75 @@ function DeckHeader({ deckId }: { deckId: string }) { ); } +/** Paginate note groups so each page contains at most CARDS_PER_PAGE cards */ +function paginateNoteGroups(items: CardDisplayItem[]): CardDisplayItem[][] { + const pages: CardDisplayItem[][] = []; + let currentPage: CardDisplayItem[] = []; + let currentCount = 0; + + for (const item of items) { + if (currentCount > 0 && currentCount + item.cards.length > CARDS_PER_PAGE) { + pages.push(currentPage); + currentPage = []; + currentCount = 0; + } + currentPage.push(item); + currentCount += item.cards.length; + } + + if (currentPage.length > 0) { + pages.push(currentPage); + } + + return pages; +} + +function Pagination({ + currentPage, + totalPages, + onPageChange, +}: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +}) { + if (totalPages <= 1) return null; + + return ( +
+ + + {currentPage + 1} / {totalPages} + + +
+ ); +} + function CardList({ deckId, onEditNote, @@ -183,6 +255,7 @@ function CardList({ onCreateNote: () => void; }) { const cards = useAtomValue(cardsByDeckAtomFamily(deckId)); + const [currentPage, setCurrentPage] = useState(0); // Group cards by note for display const displayItems = useMemo((): CardDisplayItem[] => { @@ -223,6 +296,20 @@ function CardList({ return items; }, [cards]); + const pages = useMemo(() => paginateNoteGroups(displayItems), [displayItems]); + const totalPages = pages.length; + + // Clamp current page when data changes (e.g. after deletion) + const safePage = totalPages > 0 ? Math.min(currentPage, totalPages - 1) : 0; + if (safePage !== currentPage) { + setCurrentPage(safePage); + } + + const handlePageChange = useCallback((page: number) => { + setCurrentPage(page); + window.scrollTo({ top: 0, behavior: "smooth" }); + }, []); + if (cards.length === 0) { return (
@@ -253,18 +340,27 @@ function CardList({ ); } + const pageItems = pages[safePage] ?? []; + return ( -
- {displayItems.map((item, index) => ( - onEditNote(item.noteId)} - onDeleteNote={() => onDeleteNote(item.noteId)} - /> - ))} +
+
+ {pageItems.map((item, index) => ( + onEditNote(item.noteId)} + onDeleteNote={() => onDeleteNote(item.noteId)} + /> + ))} +
+
); } -- cgit v1.3-1-g0d28