Pagination : un seul composant partagé pour les trois pages
Build & Deploy / build (push) Successful in 26s

La logique était triplicée à l'identique dans Documents, Recent Updates et
Ingredient Search, mais chaque page portait son propre style — d'où trois
rendus différents :

- IngredientSearchPage.module.css ne contenait AUCUNE classe de pagination :
  styles.pagination et consorts valaient undefined, les boutons s'affichaient
  donc entièrement sans style. C'est l'écart que tu voyais.
- DocumentsPage : règle .paginationButton malformée, un .ellipsis imbriqué
  dedans par accident de copier-coller.
- RecentUpdates : ni .ellipsis ni dimensions sur les numéros de page.

Un composant components/Pagination.tsx porte désormais la fenêtre de numéros
(extrémités toujours atteignables) et styles/Pagination.module.css le style
unique, sur l'accent bleu #2563eb du site — celui des boutons primaires, 22
occurrences contre 11 pour le violet. Les classes locales devenues mortes sont
supprimées des trois feuilles pour qu'aucun style divergent ne subsiste.

Libellés Previous/Next conservés : les pages publiques sont en anglais.

Vérifié au navigateur : même classe de module, même fond actif rgb(37,99,235),
mêmes dimensions sur les trois pages ; aucun débordement de 320 à 1280px, la
pagination passant à la ligne sous 768px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:39:36 +02:00
co-authored by Claude Opus 5
parent 9d7dadfdac
commit f170da8997
7 changed files with 1324 additions and 1404 deletions
+71
View File
@@ -0,0 +1,71 @@
import styles from "../styles/Pagination.module.css";
const MAX_VISIBLE_PAGES = 5;
type PaginationProps = {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
};
type PageEntry = number | "ellipsis";
/** Fenêtre de numéros affichés : les extrémités restent toujours atteignables. */
function buildPageList(currentPage: number, totalPages: number): PageEntry[] {
if (totalPages <= MAX_VISIBLE_PAGES) {
return Array.from({ length: totalPages }, (_, index) => index + 1);
}
if (currentPage <= 3) {
return [1, 2, 3, 4, "ellipsis", totalPages];
}
if (currentPage >= totalPages - 2) {
return [1, "ellipsis", totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
}
return [1, "ellipsis", currentPage - 1, currentPage, currentPage + 1, "ellipsis", totalPages];
}
export default function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
if (totalPages <= 1) {
return null;
}
return (
<nav className={styles.pagination} aria-label="Pagination">
<button
className={styles.navButton}
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
>
Previous
</button>
{buildPageList(currentPage, totalPages).map((entry, index) =>
entry === "ellipsis" ? (
<span key={`ellipsis-${index}`} className={styles.ellipsis} aria-hidden="true">
</span>
) : (
<button
key={entry}
className={entry === currentPage ? styles.pageActive : styles.page}
aria-current={entry === currentPage ? "page" : undefined}
onClick={() => onPageChange(entry)}
>
{entry}
</button>
)
)}
<button
className={styles.navButton}
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</nav>
);
}
+6 -59
View File
@@ -6,6 +6,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import styles from "../styles/DocumentsPage.module.css"; import styles from "../styles/DocumentsPage.module.css";
import Pagination from "../components/Pagination";
import { getDocuments } from "../services/theapi"; import { getDocuments } from "../services/theapi";
type ApiDocument = { type ApiDocument = {
@@ -247,65 +248,11 @@ export default function DocumentsPage() {
))} ))}
</div> </div>
{/* PAGINATION */} <Pagination
{totalPages > 1 && ( currentPage={safePage}
<div className={styles.pagination}> totalPages={totalPages}
<button onPageChange={setCurrentPage}
onClick={() => />
setCurrentPage(Math.max(1, safePage - 1))
}
disabled={safePage === 1}
className={styles.paginationButton}
>
Previous
</button>
{/** Render a limited set of page numbers for readability */}
{(() => {
const maxShow = 5;
const pages: (number | string)[] = [];
if (totalPages <= maxShow) {
for (let p = 1; p <= totalPages; p++) pages.push(p);
} else {
if (safePage <= 3) {
pages.push(1,2,3,4,'...', totalPages);
} else if (safePage >= totalPages - 2) {
pages.push(1,'...', totalPages-3, totalPages-2, totalPages-1, totalPages);
} else {
pages.push(1,'...', safePage-1, safePage, safePage+1,'...', totalPages);
}
}
return pages.map((p, idx) => {
if (p === '...') return <span key={`dot-${idx}`} className={styles.ellipsis}></span>;
const page = p as number;
return (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={
page === safePage
? styles.pageNumberActive
: styles.pageNumberInactive
}
>
{page}
</button>
);
});
})()}
<button
onClick={() =>
setCurrentPage(Math.min(totalPages, safePage + 1))
}
disabled={safePage === totalPages}
className={styles.paginationButton}
>
Next
</button>
</div>
)}
</div> </div>
</div> </div>
); );
+6 -20
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { Search, Filter } from "lucide-react"; import { Search, Filter } from "lucide-react";
import styles from "../styles/IngredientSearchPage.module.css"; import styles from "../styles/IngredientSearchPage.module.css";
import Pagination from "../components/Pagination";
import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; import { getDocuments, openPdf, type ApiDocument } from "../services/theapi";
export default function IngredientSearchPage() { export default function IngredientSearchPage() {
@@ -175,26 +176,11 @@ export default function IngredientSearchPage() {
</div> </div>
))} ))}
{totalPages > 1 && ( <Pagination
<div className={styles.pagination}> currentPage={safePage}
<button className={styles.paginationButton} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={currentPage===1}>Previous</button> totalPages={totalPages}
{(() => { onPageChange={setCurrentPage}
const maxShow = 5; />
const pages: (number | string)[] = [];
if (totalPages <= maxShow) {
for (let p = 1; p <= totalPages; p++) pages.push(p);
} else {
if (currentPage <= 3) pages.push(1,2,3,4,'...', totalPages);
else if (currentPage >= totalPages - 2) pages.push(1,'...', totalPages-3, totalPages-2, totalPages-1, totalPages);
else pages.push(1,'...', currentPage-1, currentPage, currentPage+1,'...', totalPages);
}
return pages.map((p, idx) => p === '...' ? <span key={`dot-${idx}`} className={styles.ellipsis}></span> : (
<button key={p} onClick={() => setCurrentPage(Number(p))} className={Number(p)===currentPage ? styles.pageNumberActive : styles.pageNumberInactive}>{p}</button>
));
})()}
<button className={styles.paginationButton} onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={currentPage===totalPages}>Next</button>
</div>
)}
</> </>
); );
})() })()
+6 -58
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { TrendingUp, Filter, Eye } from "lucide-react"; import { TrendingUp, Filter, Eye } from "lucide-react";
import styles from "../styles/RecentUpdatesPage.module.css"; import styles from "../styles/RecentUpdatesPage.module.css";
import Pagination from "../components/Pagination";
import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; import { getDocuments, openPdf, type ApiDocument } from "../services/theapi";
const DEFAULT_ITEMS_PER_PAGE = 8; const DEFAULT_ITEMS_PER_PAGE = 8;
@@ -200,64 +201,11 @@ export default function RecentUpdatesPage() {
))} ))}
</div> </div>
{/* PAGINATION */} <Pagination
{totalPages > 1 && ( currentPage={safePage}
<div className={styles.pagination}> totalPages={totalPages}
<button onPageChange={setCurrentPage}
className={styles.paginationButton} />
disabled={safePage === 1}
onClick={() =>
setCurrentPage((p) => Math.max(1, p - 1))
}
>
Previous
</button>
{(() => {
const maxShow = 5;
const pages: (number | string)[] = [];
if (totalPages <= maxShow) {
for (let p = 1; p <= totalPages; p++) pages.push(p);
} else {
if (safePage <= 3) {
pages.push(1,2,3,4,'...', totalPages);
} else if (safePage >= totalPages - 2) {
pages.push(1,'...', totalPages-3, totalPages-2, totalPages-1, totalPages);
} else {
pages.push(1,'...', safePage-1, safePage, safePage+1,'...', totalPages);
}
}
return pages.map((p, idx) => {
if (p === '...') return <span key={`dot-${idx}`} className={styles.ellipsis}></span>;
const page = p as number;
return (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={
page === safePage
? styles.pageNumberActive
: styles.pageNumberInactive
}
>
{page}
</button>
);
});
})()}
<button
className={styles.paginationButton}
disabled={safePage === totalPages}
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
>
Next
</button>
</div>
)}
</div> </div>
</div> </div>
); );
-58
View File
@@ -226,64 +226,6 @@
background-color: rgb(29, 78, 216); background-color: rgb(29, 78, 216);
} }
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1.5rem;
flex-wrap: wrap;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.paginationButton {
padding: 0.45rem 0.85rem;
font-size: 0.85rem;
border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem;
color: rgb(55, 65, 81);
.ellipsis{ display:inline-flex; align-items:center; padding:0 8px; color: rgb(107,114,128); }
background-color: white;
cursor: pointer;
transition: all 0.2s;
}
.paginationButton:hover:not(:disabled) {
background-color: rgb(249, 250, 251);
}
.paginationButton:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pageNumber {
min-width: 2rem;
padding: 0.45rem 0.75rem;
height: 2.25rem;
font-size: 0.875rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
.pageNumberActive {
background-color: rgb(37, 99, 235);
color: white;
border-color: rgb(37, 99, 235);
}
.pageNumberInactive {
border: 1px solid rgb(209, 213, 219);
color: rgb(55, 65, 81);
background-color: white;
}
.pageNumberInactive:hover {
background-color: rgb(249, 250, 251);
}
/* Mobile: make each table row a card */ /* Mobile: make each table row a card */
@media (max-width: 640px) { @media (max-width: 640px) {
+78
View File
@@ -0,0 +1,78 @@
/* Style unique de pagination, partagé par toutes les pages.
Reprend l'accent bleu #2563eb du site (boutons primaires, liens d'action). */
.pagination {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1.5rem;
}
.navButton,
.page,
.pageActive {
min-width: 2.25rem;
height: 2.25rem;
padding: 0 0.75rem;
font-size: 0.875rem;
font-weight: 500;
font-family: inherit;
border-radius: 0.5rem;
border: 1px solid rgb(209, 213, 219);
background-color: white;
color: rgb(55, 65, 81);
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}
.navButton:hover:not(:disabled),
.page:hover {
background-color: rgb(249, 250, 251);
border-color: rgb(156, 163, 175);
}
.navButton:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pageActive {
background-color: rgb(37, 99, 235);
border-color: rgb(37, 99, 235);
color: white;
}
.pageActive:hover {
background-color: rgb(29, 78, 216);
border-color: rgb(29, 78, 216);
}
.navButton:focus-visible,
.page:focus-visible,
.pageActive:focus-visible {
outline: 2px solid rgb(37, 99, 235);
outline-offset: 2px;
}
.ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 2.25rem;
color: rgb(107, 114, 128);
user-select: none;
}
@media (max-width: 480px) {
.navButton,
.page,
.pageActive {
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
font-size: 0.8125rem;
}
}
-52
View File
@@ -248,58 +248,6 @@
background-color: rgb(29, 78, 216); background-color: rgb(29, 78, 216);
} }
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1.5rem;
}
.paginationButton {
padding: 0.5rem 1rem;
font-size: 0.875rem;
border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem;
color: rgb(55, 65, 81);
background-color: white;
cursor: pointer;
transition: all 0.2s;
}
.paginationButton:hover:not(:disabled) {
background-color: rgb(249, 250, 251);
}
.paginationButton:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.pageNumber {
width: 2.25rem;
height: 2.25rem;
font-size: 0.875rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s;
}
.pageNumberActive {
background-color: rgb(37, 99, 235);
color: white;
}
.pageNumberInactive {
border: 1px solid rgb(209, 213, 219);
color: rgb(55, 65, 81);
background-color: white;
}
.pageNumberInactive:hover {
background-color: rgb(249, 250, 251);
}
@media (max-width: 1024px) { @media (max-width: 1024px) {
.statsGrid { .statsGrid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);