Pagination : un seul composant partagé pour les trois pages
Build & Deploy / build (push) Successful in 26s
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:
@@ -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
@@ -6,6 +6,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import styles from "../styles/DocumentsPage.module.css";
|
||||
import Pagination from "../components/Pagination";
|
||||
import { getDocuments } from "../services/theapi";
|
||||
|
||||
type ApiDocument = {
|
||||
@@ -247,65 +248,11 @@ export default function DocumentsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* PAGINATION */}
|
||||
{totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
<Pagination
|
||||
currentPage={safePage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Search, Filter } from "lucide-react";
|
||||
import styles from "../styles/IngredientSearchPage.module.css";
|
||||
import Pagination from "../components/Pagination";
|
||||
import { getDocuments, openPdf, type ApiDocument } from "../services/theapi";
|
||||
|
||||
export default function IngredientSearchPage() {
|
||||
@@ -175,26 +176,11 @@ export default function IngredientSearchPage() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button className={styles.paginationButton} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={currentPage===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 (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>
|
||||
)}
|
||||
<Pagination
|
||||
currentPage={safePage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { TrendingUp, Filter, Eye } from "lucide-react";
|
||||
import styles from "../styles/RecentUpdatesPage.module.css";
|
||||
import Pagination from "../components/Pagination";
|
||||
import { getDocuments, openPdf, type ApiDocument } from "../services/theapi";
|
||||
|
||||
const DEFAULT_ITEMS_PER_PAGE = 8;
|
||||
@@ -200,64 +201,11 @@ export default function RecentUpdatesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* PAGINATION */}
|
||||
{totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
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>
|
||||
)}
|
||||
<Pagination
|
||||
currentPage={safePage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={setCurrentPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -226,64 +226,6 @@
|
||||
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 */
|
||||
@media (max-width: 640px) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -248,58 +248,6 @@
|
||||
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) {
|
||||
.statsGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user