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";
|
} 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user