diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..7e51dc8 --- /dev/null +++ b/src/components/Pagination.tsx @@ -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 ( + + ); +} diff --git a/src/pages/Documents.tsx b/src/pages/Documents.tsx index 6c3b738..d7a0981 100644 --- a/src/pages/Documents.tsx +++ b/src/pages/Documents.tsx @@ -1,312 +1,259 @@ -import { useEffect, useMemo, useState } from "react"; -import { - Filter, - ArrowUpDown, - File as FileIcon, -} from "lucide-react"; - -import styles from "../styles/DocumentsPage.module.css"; -import { getDocuments } from "../services/theapi"; - -type ApiDocument = { - title: string; - ingredient: string; - source: string; - type: string; - date?: string | null; - pdf_url: string; - bold?: boolean; -}; - -const DEFAULT_ITEMS_PER_PAGE = 8; - -export default function DocumentsPage() { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(true); - const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); - - const [sourceFilter, setSourceFilter] = useState("All Sources"); - const [typeFilter, setTypeFilter] = useState("All Types"); - const [dateFilter, setDateFilter] = useState("All Dates"); - const [sortBy, setSortBy] = useState("Date (Newest)"); - const [currentPage, setCurrentPage] = useState(1); - - useEffect(() => { - getDocuments() - .then((data) => { - setDocuments(Array.isArray(data) ? data : []); - }) - .catch((err) => { - console.error("API Error:", err); - setDocuments([]); - }) - .finally(() => setLoading(false)); - }, []); - - const allSources = useMemo( - () => ["All Sources", ...new Set(documents.map((d) => d.source))], - [documents] - ); - - const allTypes = useMemo( - () => ["All Types", ...new Set(documents.map((d) => d.type))], - [documents] - ); - - const allDates = useMemo( - () => [ - "All Dates", - ...new Set( - documents.map((d) => String(d.date ?? "").slice(0, 4)) - ), - ], - [documents] - ); - - const filtered = useMemo(() => { - return documents - .filter((d) => { - const sourceMatch = - sourceFilter === "All Sources" || d.source === sourceFilter; - - const typeMatch = - typeFilter === "All Types" || d.type === typeFilter; - - const safeDate = String(d.date ?? ""); - const dateMatch = - dateFilter === "All Dates" || - safeDate.startsWith(dateFilter); - - return sourceMatch && typeMatch && dateMatch; - }) - .sort((a, b) => { - const dateA = String(a.date ?? ""); - const dateB = String(b.date ?? ""); - const titleA = String(a.title ?? ""); - const titleB = String(b.title ?? ""); - - if (sortBy === "Date (Newest)") return dateB.localeCompare(dateA); - if (sortBy === "Date (Oldest)") return dateA.localeCompare(dateB); - if (sortBy === "Title (A-Z)") return titleA.localeCompare(titleB); - if (sortBy === "Title (Z-A)") return titleB.localeCompare(titleA); - return 0; - }); - }, [documents, sourceFilter, typeFilter, dateFilter, sortBy]); - - const totalPages = Math.max( - 1, - Math.ceil(filtered.length / itemsPerPage) - ); - - const safePage = Math.min(currentPage, totalPages); - - const paginated = filtered.slice( - (safePage - 1) * itemsPerPage, - safePage * itemsPerPage - ); - - // adjust items per page based on viewport width - useEffect(() => { - const apply = () => { - const w = window.innerWidth; - if (w < 640) setItemsPerPage(3); - else if (w < 900) setItemsPerPage(6); - else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); - }; - apply(); - window.addEventListener('resize', apply); - return () => window.removeEventListener('resize', apply); - }, []); - - if (loading) { - return ( -
-

Loading documents...

-
- ); - } - - return ( -
-
- {/* HEADER */} -
-

Document Database

-

- Complete collection of regulatory monitoring documents -

-
- - {/* FILTERS */} -
-
- - Filters: -
- - - - - - - -
- - -
-
- - {/* STATS */} -
-

- Total Documents: - - {" "} - {filtered.length} - -

-
- - {/* TABLE */} -
-
- Title - Ingredient - Source - Type - Date - PDF -
- - {paginated.map((doc, index) => ( -
-
- - {doc.title} -
- - {doc.ingredient} - {doc.source} - {doc.type} - {doc.date} - - -
- ))} -
- - {/* PAGINATION */} - {totalPages > 1 && ( -
- - - {/** 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 ; - const page = p as number; - return ( - - ); - }); - })()} - - -
- )} -
-
- ); +import { useEffect, useMemo, useState } from "react"; +import { + Filter, + ArrowUpDown, + File as FileIcon, +} from "lucide-react"; + +import styles from "../styles/DocumentsPage.module.css"; +import Pagination from "../components/Pagination"; +import { getDocuments } from "../services/theapi"; + +type ApiDocument = { + title: string; + ingredient: string; + source: string; + type: string; + date?: string | null; + pdf_url: string; + bold?: boolean; +}; + +const DEFAULT_ITEMS_PER_PAGE = 8; + +export default function DocumentsPage() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); + + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [dateFilter, setDateFilter] = useState("All Dates"); + const [sortBy, setSortBy] = useState("Date (Newest)"); + const [currentPage, setCurrentPage] = useState(1); + + useEffect(() => { + getDocuments() + .then((data) => { + setDocuments(Array.isArray(data) ? data : []); + }) + .catch((err) => { + console.error("API Error:", err); + setDocuments([]); + }) + .finally(() => setLoading(false)); + }, []); + + const allSources = useMemo( + () => ["All Sources", ...new Set(documents.map((d) => d.source))], + [documents] + ); + + const allTypes = useMemo( + () => ["All Types", ...new Set(documents.map((d) => d.type))], + [documents] + ); + + const allDates = useMemo( + () => [ + "All Dates", + ...new Set( + documents.map((d) => String(d.date ?? "").slice(0, 4)) + ), + ], + [documents] + ); + + const filtered = useMemo(() => { + return documents + .filter((d) => { + const sourceMatch = + sourceFilter === "All Sources" || d.source === sourceFilter; + + const typeMatch = + typeFilter === "All Types" || d.type === typeFilter; + + const safeDate = String(d.date ?? ""); + const dateMatch = + dateFilter === "All Dates" || + safeDate.startsWith(dateFilter); + + return sourceMatch && typeMatch && dateMatch; + }) + .sort((a, b) => { + const dateA = String(a.date ?? ""); + const dateB = String(b.date ?? ""); + const titleA = String(a.title ?? ""); + const titleB = String(b.title ?? ""); + + if (sortBy === "Date (Newest)") return dateB.localeCompare(dateA); + if (sortBy === "Date (Oldest)") return dateA.localeCompare(dateB); + if (sortBy === "Title (A-Z)") return titleA.localeCompare(titleB); + if (sortBy === "Title (Z-A)") return titleB.localeCompare(titleA); + return 0; + }); + }, [documents, sourceFilter, typeFilter, dateFilter, sortBy]); + + const totalPages = Math.max( + 1, + Math.ceil(filtered.length / itemsPerPage) + ); + + const safePage = Math.min(currentPage, totalPages); + + const paginated = filtered.slice( + (safePage - 1) * itemsPerPage, + safePage * itemsPerPage + ); + + // adjust items per page based on viewport width + useEffect(() => { + const apply = () => { + const w = window.innerWidth; + if (w < 640) setItemsPerPage(3); + else if (w < 900) setItemsPerPage(6); + else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); + }; + apply(); + window.addEventListener('resize', apply); + return () => window.removeEventListener('resize', apply); + }, []); + + if (loading) { + return ( +
+

Loading documents...

+
+ ); + } + + return ( +
+
+ {/* HEADER */} +
+

Document Database

+

+ Complete collection of regulatory monitoring documents +

+
+ + {/* FILTERS */} +
+
+ + Filters: +
+ + + + + + + +
+ + +
+
+ + {/* STATS */} +
+

+ Total Documents: + + {" "} + {filtered.length} + +

+
+ + {/* TABLE */} +
+
+ Title + Ingredient + Source + Type + Date + PDF +
+ + {paginated.map((doc, index) => ( +
+
+ + {doc.title} +
+ + {doc.ingredient} + {doc.source} + {doc.type} + {doc.date} + + +
+ ))} +
+ + +
+
+ ); } \ No newline at end of file diff --git a/src/pages/IngredientSearchPage.tsx b/src/pages/IngredientSearchPage.tsx index a7e761c..e712231 100644 --- a/src/pages/IngredientSearchPage.tsx +++ b/src/pages/IngredientSearchPage.tsx @@ -1,206 +1,192 @@ -import { useEffect, useMemo, useState } from "react"; -import { Search, Filter } from "lucide-react"; -import styles from "../styles/IngredientSearchPage.module.css"; -import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; - -export default function IngredientSearchPage() { - const [documents, setDocuments] = useState([]); - const [loading, setLoading] = useState(true); - - const [searchQuery, setSearchQuery] = useState(""); - const [sourceFilter, setSourceFilter] = useState("All Sources"); - const [typeFilter, setTypeFilter] = useState("All Types"); - const [dateFilter, setDateFilter] = useState("All Dates"); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(8); - - // FETCH API - useEffect(() => { - getDocuments() - .then((data) => setDocuments(Array.isArray(data) ? data : [])) - .catch((err) => { - console.error("API ERROR:", err); - setDocuments([]); - }) - .finally(() => setLoading(false)); - }, []); - - // responsive items per page - useEffect(() => { - const apply = () => { - const w = window.innerWidth; - if (w < 640) setItemsPerPage(3); - else if (w < 900) setItemsPerPage(6); - else setItemsPerPage(8); - }; - apply(); - window.addEventListener('resize', apply); - return () => window.removeEventListener('resize', apply); - }, []); - - // FILTER OPTIONS - const allSources = useMemo(() => [ - "All Sources", - ...new Set(documents.map((d) => d.source)), - ], [documents]); - - const allTypes = useMemo(() => [ - "All Types", - ...new Set(documents.map((d) => d.type)), - ], [documents]); - - const allDates = useMemo(() => [ - "All Dates", - ...new Set(documents.map((d) => String(d.date ?? "").slice(0, 4))).values(), - ], [documents]); - - // FILTERED RESULTS - const filtered = useMemo(() => { - return documents - .filter((d) => { - const queryMatch = - searchQuery === "" || - d.ingredient.toLowerCase().includes(searchQuery.toLowerCase()) || - d.title.toLowerCase().includes(searchQuery.toLowerCase()); - - const sourceMatch = sourceFilter === "All Sources" || d.source === sourceFilter; - const typeMatch = typeFilter === "All Types" || d.type === typeFilter; - const safeDate = String(d.date ?? ""); - const dateMatch = dateFilter === "All Dates" || safeDate.startsWith(dateFilter); - - return queryMatch && sourceMatch && typeMatch && dateMatch; - }) - .sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? ""))); - }, [documents, searchQuery, sourceFilter, typeFilter, dateFilter]); - - if (loading) { - return

Loading...

; - } - - return ( -
-
- - {/* HEADER */} -
-

Ingredient Search

-

Search cosmetic ingredients and regulatory documents

-
- - {/* SEARCH */} -
-
- setSearchQuery(e.target.value)} - /> - -
-
- - {/* FILTERS */} -
-
- - Filters -
-
- - - -
-
- - {/* RESULTS */} -
- {filtered.length} results found -
- - {/* DOCUMENTS */} -
- {filtered.length === 0 ? ( -
-

No documents found.

-
- ) : ( - (() => { - const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); - const safePage = Math.min(currentPage, totalPages); - const paginated = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage); - return ( - <> - {paginated.map((doc) => ( -
-
-
-

{doc.title}

-
-
- Ingredient - {doc.ingredient} -
-
- Source - {doc.source} -
-
- Type - {doc.type} -
-
- Date - {doc.date ?? "—"} -
-
-
- - {doc.pdf_url && ( - - )} -
-
- ))} - - {totalPages > 1 && ( -
- - {(() => { - 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 === '...' ? : ( - - )); - })()} - -
- )} - - ); - })() - )} -
-
-
- ); +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() { + const [documents, setDocuments] = useState([]); + const [loading, setLoading] = useState(true); + + const [searchQuery, setSearchQuery] = useState(""); + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [dateFilter, setDateFilter] = useState("All Dates"); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(8); + + // FETCH API + useEffect(() => { + getDocuments() + .then((data) => setDocuments(Array.isArray(data) ? data : [])) + .catch((err) => { + console.error("API ERROR:", err); + setDocuments([]); + }) + .finally(() => setLoading(false)); + }, []); + + // responsive items per page + useEffect(() => { + const apply = () => { + const w = window.innerWidth; + if (w < 640) setItemsPerPage(3); + else if (w < 900) setItemsPerPage(6); + else setItemsPerPage(8); + }; + apply(); + window.addEventListener('resize', apply); + return () => window.removeEventListener('resize', apply); + }, []); + + // FILTER OPTIONS + const allSources = useMemo(() => [ + "All Sources", + ...new Set(documents.map((d) => d.source)), + ], [documents]); + + const allTypes = useMemo(() => [ + "All Types", + ...new Set(documents.map((d) => d.type)), + ], [documents]); + + const allDates = useMemo(() => [ + "All Dates", + ...new Set(documents.map((d) => String(d.date ?? "").slice(0, 4))).values(), + ], [documents]); + + // FILTERED RESULTS + const filtered = useMemo(() => { + return documents + .filter((d) => { + const queryMatch = + searchQuery === "" || + d.ingredient.toLowerCase().includes(searchQuery.toLowerCase()) || + d.title.toLowerCase().includes(searchQuery.toLowerCase()); + + const sourceMatch = sourceFilter === "All Sources" || d.source === sourceFilter; + const typeMatch = typeFilter === "All Types" || d.type === typeFilter; + const safeDate = String(d.date ?? ""); + const dateMatch = dateFilter === "All Dates" || safeDate.startsWith(dateFilter); + + return queryMatch && sourceMatch && typeMatch && dateMatch; + }) + .sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? ""))); + }, [documents, searchQuery, sourceFilter, typeFilter, dateFilter]); + + if (loading) { + return

Loading...

; + } + + return ( +
+
+ + {/* HEADER */} +
+

Ingredient Search

+

Search cosmetic ingredients and regulatory documents

+
+ + {/* SEARCH */} +
+
+ setSearchQuery(e.target.value)} + /> + +
+
+ + {/* FILTERS */} +
+
+ + Filters +
+
+ + + +
+
+ + {/* RESULTS */} +
+ {filtered.length} results found +
+ + {/* DOCUMENTS */} +
+ {filtered.length === 0 ? ( +
+

No documents found.

+
+ ) : ( + (() => { + const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); + const safePage = Math.min(currentPage, totalPages); + const paginated = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage); + return ( + <> + {paginated.map((doc) => ( +
+
+
+

{doc.title}

+
+
+ Ingredient + {doc.ingredient} +
+
+ Source + {doc.source} +
+
+ Type + {doc.type} +
+
+ Date + {doc.date ?? "—"} +
+
+
+ + {doc.pdf_url && ( + + )} +
+
+ ))} + + + + ); + })() + )} +
+
+
+ ); } \ No newline at end of file diff --git a/src/pages/RecentUpdatesPage.tsx b/src/pages/RecentUpdatesPage.tsx index bd5942b..fc794b8 100644 --- a/src/pages/RecentUpdatesPage.tsx +++ b/src/pages/RecentUpdatesPage.tsx @@ -1,264 +1,212 @@ -import { useEffect, useMemo, useState } from "react"; -import { TrendingUp, Filter, Eye } from "lucide-react"; -import styles from "../styles/RecentUpdatesPage.module.css"; -import { getDocuments, openPdf, type ApiDocument } from "../services/theapi"; - -const DEFAULT_ITEMS_PER_PAGE = 8; - -export default function RecentUpdatesPage() { - const [updates, setUpdates] = useState([]); - const [loading, setLoading] = useState(true); - const [sourceFilter, setSourceFilter] = useState("All Sources"); - const [typeFilter, setTypeFilter] = useState("All Types"); - const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); - - useEffect(() => { - getDocuments() - .then((data) => setUpdates(Array.isArray(data) ? data : [])) - .catch((err) => { - console.error("API ERROR:", err); - setUpdates([]); - }) - .finally(() => setLoading(false)); - }, []); - - const allSources = useMemo( - () => ["All Sources", ...new Set(updates.map((u) => u.source))], - [updates] - ); - - const allTypes = useMemo( - () => ["All Types", ...new Set(updates.map((u) => u.type))], - [updates] - ); - - const filtered = useMemo(() => { - return updates - .filter((u) => { - const sourceMatch = - sourceFilter === "All Sources" || u.source === sourceFilter; - - const typeMatch = - typeFilter === "All Types" || u.type === typeFilter; - - return sourceMatch && typeMatch; - }) - .sort((a, b) => - String(b.date ?? "").localeCompare(String(a.date ?? "")) - ); - }, [updates, sourceFilter, typeFilter]); - - const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); - const safePage = Math.min(currentPage, totalPages); - - const paginated = filtered.slice( - (safePage - 1) * itemsPerPage, - safePage * itemsPerPage - ); - - useEffect(() => { - const apply = () => { - const w = window.innerWidth; - if (w < 640) setItemsPerPage(3); - else if (w < 900) setItemsPerPage(6); - else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); - }; - apply(); - window.addEventListener('resize', apply); - return () => window.removeEventListener('resize', apply); - }, []); - - if (loading) { - return ( -
-

Loading updates...

-
- ); - } - - return ( -
-
- {/* HEADER */} -
-
- -

Recent Updates

-
-

- Latest cosmetic regulatory updates and monitoring activity -

-
- - {/* STATS */} -
-
-

Total Updates

-

{updates.length}

-
- -
-
- -
-

Sources

-

{allSources.length - 1}

-
- -
-
-
- - {/* FILTERS */} -
-
- - Filters -
- - - - -
- - {/* RESULTS */} -
- {filtered.length}{" "} - updates found -
- - {/* TABLE */} -
-
- Status - Title - Ingredient - Source - Type - Date - View -
- - {paginated.map((u, index) => ( -
-
- - New - -
- -
- {u.title} -
- -
{u.ingredient}
-
{u.source}
-
{u.type}
-
{u.date ?? "—"}
- -
- {u.pdf_url && ( - - )} -
-
- ))} -
- - {/* PAGINATION */} - {totalPages > 1 && ( -
- - - {(() => { - 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 ; - const page = p as number; - return ( - - ); - }); - })()} - - -
- )} -
-
- ); +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; + +export default function RecentUpdatesPage() { + const [updates, setUpdates] = useState([]); + const [loading, setLoading] = useState(true); + const [sourceFilter, setSourceFilter] = useState("All Sources"); + const [typeFilter, setTypeFilter] = useState("All Types"); + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); + + useEffect(() => { + getDocuments() + .then((data) => setUpdates(Array.isArray(data) ? data : [])) + .catch((err) => { + console.error("API ERROR:", err); + setUpdates([]); + }) + .finally(() => setLoading(false)); + }, []); + + const allSources = useMemo( + () => ["All Sources", ...new Set(updates.map((u) => u.source))], + [updates] + ); + + const allTypes = useMemo( + () => ["All Types", ...new Set(updates.map((u) => u.type))], + [updates] + ); + + const filtered = useMemo(() => { + return updates + .filter((u) => { + const sourceMatch = + sourceFilter === "All Sources" || u.source === sourceFilter; + + const typeMatch = + typeFilter === "All Types" || u.type === typeFilter; + + return sourceMatch && typeMatch; + }) + .sort((a, b) => + String(b.date ?? "").localeCompare(String(a.date ?? "")) + ); + }, [updates, sourceFilter, typeFilter]); + + const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); + const safePage = Math.min(currentPage, totalPages); + + const paginated = filtered.slice( + (safePage - 1) * itemsPerPage, + safePage * itemsPerPage + ); + + useEffect(() => { + const apply = () => { + const w = window.innerWidth; + if (w < 640) setItemsPerPage(3); + else if (w < 900) setItemsPerPage(6); + else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); + }; + apply(); + window.addEventListener('resize', apply); + return () => window.removeEventListener('resize', apply); + }, []); + + if (loading) { + return ( +
+

Loading updates...

+
+ ); + } + + return ( +
+
+ {/* HEADER */} +
+
+ +

Recent Updates

+
+

+ Latest cosmetic regulatory updates and monitoring activity +

+
+ + {/* STATS */} +
+
+

Total Updates

+

{updates.length}

+
+ +
+
+ +
+

Sources

+

{allSources.length - 1}

+
+ +
+
+
+ + {/* FILTERS */} +
+
+ + Filters +
+ + + + +
+ + {/* RESULTS */} +
+ {filtered.length}{" "} + updates found +
+ + {/* TABLE */} +
+
+ Status + Title + Ingredient + Source + Type + Date + View +
+ + {paginated.map((u, index) => ( +
+
+ + New + +
+ +
+ {u.title} +
+ +
{u.ingredient}
+
{u.source}
+
{u.type}
+
{u.date ?? "—"}
+ +
+ {u.pdf_url && ( + + )} +
+
+ ))} +
+ + +
+
+ ); } \ No newline at end of file diff --git a/src/styles/DocumentsPage.module.css b/src/styles/DocumentsPage.module.css index c25a491..f65aba7 100644 --- a/src/styles/DocumentsPage.module.css +++ b/src/styles/DocumentsPage.module.css @@ -1,295 +1,237 @@ -.container { - padding: 2rem; -} - -.maxWidth { - max-width: 80rem; -} - -.header { - margin-bottom: 1.75rem; -} - -.title { - font-size: 2.25rem; - font-weight: 700; - color: rgb(17, 24, 39); - margin-bottom: 0.5rem; - line-height: 1.1; -} - -.description { - color: rgb(75, 85, 99); - margin-bottom: 1.5rem; - font-size: 1.125rem; - line-height: 1.6; -} - -.filtersBox { - background-color: white; - border: 1px solid rgb(229, 231, 235); - border-radius: 0.75rem; - padding: 1.25rem; - margin-bottom: 1.25rem; - display: flex; - align-items: center; - gap: 0.75rem; - flex-wrap: wrap; -} - -.filterLabel { - display: flex; - align-items: center; - gap: 0.5rem; - color: rgb(55, 65, 81); - font-weight: 500; - font-size: 0.875rem; -} - -.filterIcon { - font-size: 1rem; -} - -.select { - border: 1px solid rgb(209, 213, 219); - border-radius: 0.5rem; - font-size: 0.875rem; - padding: 0.375rem 0.75rem; - color: rgb(55, 65, 81); - background-color: white; - cursor: pointer; -} - -.select:focus { - outline: none; - box-shadow: 0 0 0 2px rgb(59, 130, 246); -} - -.sortWrapper { - margin-left: auto; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.sortIcon { - color: rgb(107, 114, 128); -} - -.statsRow { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.75rem; -} - -.statsText { - font-size: 0.875rem; - color: rgb(55, 65, 81); -} - -.statsNumber { - font-weight: 600; -} - -.exportButton { - display: flex; - align-items: center; - gap: 0.5rem; - border: 1px solid rgb(209, 213, 219); - border-radius: 0.5rem; - font-size: 0.875rem; - padding: 0.375rem 1rem; - color: rgb(55, 65, 81); - background-color: white; - cursor: pointer; - transition: background-color 0.2s; -} - -.exportButton:hover { - background-color: rgb(249, 250, 251); -} - -.table { - background-color: white; - border: 1px solid rgb(229, 231, 235); - border-radius: 0.75rem; - overflow: auto; - -webkit-overflow-scrolling: touch; -} - -.tableHeader { - display: grid; - grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; - background-color: rgb(249, 250, 251); - border-bottom: 1px solid rgb(229, 231, 235); - padding: 0.75rem 1.25rem; - color: rgb(17, 24, 39); -} - -.tableRow { - display: grid; - grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; - padding: 1rem 1.25rem; - align-items: center; - color: rgb(17, 24, 39); -} - -.tableRow span, -.titleCell span, -.titleCell { - color: rgb(17, 24, 39); -} - -.headerCell { - font-size: 0.875rem; - font-weight: 600; - color: rgb(55, 65, 81); -} - -.tableRow { - display: grid; - grid-template-columns: 2fr 1.2fr 1fr 1.2fr 0.8fr 0.6fr; - padding: 1rem 1.25rem; - align-items: center; -} - -.tableRow:hover { - background-color: rgb(249, 250, 251); - transition: background-color 0.2s; -} - -.tableRowBorder { - border-bottom: 1px solid rgb(229, 231, 235); -} - -.titleCell { - display: flex; - align-items: flex-start; - gap: 0.625rem; -} - -.titleIcon { - color: rgb(156, 163, 175); - margin-top: 0.125rem; - flex-shrink: 0; -} - -.titleText { - font-size: 0.875rem; - color: rgb(31, 41, 55); - line-height: 1.5; -} - -.ingredientBadge { - display: inline-block; - font-size: 0.75rem; - padding: 0.625rem; - border-radius: 0.375rem; - border: 1px solid rgb(209, 213, 219); - background-color: rgb(243, 244, 246); -} - -.ingredientBadgeBold { - font-weight: 600; - border-color: rgb(209, 213, 219); - color: rgb(17, 24, 39); -} - -.ingredientBadgeNormal { - border-color: rgb(229, 231, 235); - color: rgb(55, 65, 81); -} - -.cell { - font-size: 0.875rem; - color: rgb(17, 24, 39); -} - -.cellWhitespace { - white-space: nowrap; -} - -.openButton { - background-color: rgb(37, 99, 235); - color: white; - font-size: 0.875rem; - font-weight: 500; - padding: 0.375rem 1rem; - border-radius: 0.5rem; - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.openButton:hover { - 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) { - .tableHeader { display: none; } - .tableRow { display: block; padding: 12px; border-bottom: 1px solid rgb(229,231,235); } - .titleCell { font-weight:700; font-size:1rem; display:flex; align-items:center; gap:8px } - .tableRow span { display:block; margin-top:6px; color: rgb(17,24,39) } - .openButton { width:100%; display:block } -} +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 80rem; +} + +.header { + margin-bottom: 1.75rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + margin-bottom: 0.5rem; + line-height: 1.1; +} + +.description { + color: rgb(75, 85, 99); + margin-bottom: 1.5rem; + font-size: 1.125rem; + line-height: 1.6; +} + +.filtersBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; + margin-bottom: 1.25rem; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.filterLabel { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgb(55, 65, 81); + font-weight: 500; + font-size: 0.875rem; +} + +.filterIcon { + font-size: 1rem; +} + +.select { + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; +} + +.select:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.sortWrapper { + margin-left: auto; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.sortIcon { + color: rgb(107, 114, 128); +} + +.statsRow { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.statsText { + font-size: 0.875rem; + color: rgb(55, 65, 81); +} + +.statsNumber { + font-weight: 600; +} + +.exportButton { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 1rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.exportButton:hover { + background-color: rgb(249, 250, 251); +} + +.table { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.tableHeader { + display: grid; + grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + background-color: rgb(249, 250, 251); + border-bottom: 1px solid rgb(229, 231, 235); + padding: 0.75rem 1.25rem; + color: rgb(17, 24, 39); +} + +.tableRow { + display: grid; + grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + padding: 1rem 1.25rem; + align-items: center; + color: rgb(17, 24, 39); +} + +.tableRow span, +.titleCell span, +.titleCell { + color: rgb(17, 24, 39); +} + +.headerCell { + font-size: 0.875rem; + font-weight: 600; + color: rgb(55, 65, 81); +} + +.tableRow { + display: grid; + grid-template-columns: 2fr 1.2fr 1fr 1.2fr 0.8fr 0.6fr; + padding: 1rem 1.25rem; + align-items: center; +} + +.tableRow:hover { + background-color: rgb(249, 250, 251); + transition: background-color 0.2s; +} + +.tableRowBorder { + border-bottom: 1px solid rgb(229, 231, 235); +} + +.titleCell { + display: flex; + align-items: flex-start; + gap: 0.625rem; +} + +.titleIcon { + color: rgb(156, 163, 175); + margin-top: 0.125rem; + flex-shrink: 0; +} + +.titleText { + font-size: 0.875rem; + color: rgb(31, 41, 55); + line-height: 1.5; +} + +.ingredientBadge { + display: inline-block; + font-size: 0.75rem; + padding: 0.625rem; + border-radius: 0.375rem; + border: 1px solid rgb(209, 213, 219); + background-color: rgb(243, 244, 246); +} + +.ingredientBadgeBold { + font-weight: 600; + border-color: rgb(209, 213, 219); + color: rgb(17, 24, 39); +} + +.ingredientBadgeNormal { + border-color: rgb(229, 231, 235); + color: rgb(55, 65, 81); +} + +.cell { + font-size: 0.875rem; + color: rgb(17, 24, 39); +} + +.cellWhitespace { + white-space: nowrap; +} + +.openButton { + background-color: rgb(37, 99, 235); + color: white; + font-size: 0.875rem; + font-weight: 500; + padding: 0.375rem 1rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.openButton:hover { + background-color: rgb(29, 78, 216); +} + + +/* Mobile: make each table row a card */ +@media (max-width: 640px) { + .tableHeader { display: none; } + .tableRow { display: block; padding: 12px; border-bottom: 1px solid rgb(229,231,235); } + .titleCell { font-weight:700; font-size:1rem; display:flex; align-items:center; gap:8px } + .tableRow span { display:block; margin-top:6px; color: rgb(17,24,39) } + .openButton { width:100%; display:block } +} diff --git a/src/styles/Pagination.module.css b/src/styles/Pagination.module.css new file mode 100644 index 0000000..eb2abf1 --- /dev/null +++ b/src/styles/Pagination.module.css @@ -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; + } +} diff --git a/src/styles/RecentUpdatesPage.module.css b/src/styles/RecentUpdatesPage.module.css index 7e1d781..701e5a8 100644 --- a/src/styles/RecentUpdatesPage.module.css +++ b/src/styles/RecentUpdatesPage.module.css @@ -1,330 +1,278 @@ -.container { - padding: 2rem; -} - -.maxWidth { - max-width: 96rem; -} - -.header { - margin-bottom: 2rem; -} - -.headerFlex { - display: flex; - align-items: center; - gap: 0.75rem; - margin-bottom: 0.25rem; -} - -.headerIcon { - color: rgb(37, 99, 235); - font-size: 1.75rem; -} - -.title { - font-size: 2.25rem; - font-weight: 700; - color: rgb(17, 24, 39); - line-height: 1.1; -} - -.subtitle { - color: rgb(75, 85, 99); - margin-bottom: 2rem; - font-size: 1.125rem; - line-height: 1.6; -} - -.statsGrid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 1rem; - margin-bottom: 2rem; -} - -.statCard { - background-color: white; - border: 1px solid rgb(229, 231, 235); - border-radius: 0.75rem; - padding: 1.25rem; -} - -.statLabel { - color: rgb(75, 85, 99); - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 0.5rem; -} - -.statValue { - font-size: 1.875rem; - font-weight: bold; - color: rgb(17, 24, 39); -} - -.statIcon { - margin-top: 0.75rem; - width: 2rem; - height: 2rem; - border-radius: 0.5rem; - display: flex; - align-items: center; - justify-content: center; -} - -.statIconBlue { - background-color: rgb(219, 234, 254); - color: rgb(37, 99, 235); -} - -.statIconGreen { - background-color: rgb(220, 252, 231); - color: rgb(34, 197, 94); -} - -.filtersBox { - background-color: white; - border: 1px solid rgb(229, 231, 235); - border-radius: 0.75rem; - padding: 1.5rem; - margin-bottom: 1.5rem; - display: flex; - align-items: center; - gap: 0.75rem; - flex-wrap: wrap; -} - -.filterLabel { - display: flex; - align-items: center; - gap: 0.5rem; - color: rgb(55, 65, 81); - font-weight: 500; - font-size: 0.875rem; -} - -.filterIcon { - font-size: 1rem; -} - -.select { - border: 1px solid rgb(209, 213, 219); - border-radius: 0.5rem; - font-size: 0.875rem; - padding: 0.375rem 0.75rem; - color: rgb(55, 65, 81); - background-color: white; - cursor: pointer; -} - -.select:focus { - outline: none; - box-shadow: 0 0 0 2px rgb(59, 130, 246); -} - -.exportWrapper { - margin-left: auto; -} - -.exportButton { - display: flex; - align-items: center; - gap: 0.5rem; - border: 1px solid rgb(209, 213, 219); - border-radius: 0.5rem; - font-size: 0.875rem; - padding: 0.375rem 1rem; - color: rgb(55, 65, 81); - background-color: white; - cursor: pointer; - transition: background-color 0.2s; -} - -.exportButton:hover { - background-color: rgb(249, 250, 251); -} - -.resultsInfo { - color: rgb(75, 85, 99); - margin-bottom: 1rem; - font-size: 0.875rem; -} - -.resultsCount { - font-weight: 600; -} - -.table { - background-color: white; - border: 1px solid rgb(229, 231, 235); - border-radius: 0.75rem; - overflow: auto; - -webkit-overflow-scrolling: touch; -} - -.tableHeader { - display: grid; - grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; - gap: 1rem; - background-color: rgb(249, 250, 251); - border-bottom: 1px solid rgb(229, 231, 235); - padding: 0.75rem 1.5rem; -} - -.headerCell { - font-size: 0.875rem; - font-weight: 600; - color: rgb(55, 65, 81); -} - -.tableRow { - display: grid; - grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; - gap: 1rem; - padding: 1rem 1.5rem; - align-items: center; -} - -.tableRow:hover { - background-color: rgb(249, 250, 251); - transition: background-color 0.2s; -} - -.tableRowBorder { - border-bottom: 1px solid rgb(229, 231, 235); -} - -.statusBadge { - display: inline-block; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 600; -} - -.statusBadgeNew { - background-color: rgb(220, 252, 231); - color: rgb(22, 163, 74); -} - -.statusBadgeUpdated { - background-color: rgb(219, 234, 254); - color: rgb(37, 99, 235); -} - -.titleCell { - font-weight: 500; - color: rgb(17, 24, 39); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.cell { - font-size: 0.875rem; - color: rgb(75, 85, 99); -} - -.cellTruncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.viewButton { - background-color: rgb(37, 99, 235); - color: white; - font-size: 0.75rem; - font-weight: 500; - padding: 0.375rem 0.75rem; - border-radius: 0.375rem; - border: none; - cursor: pointer; - transition: background-color 0.2s; -} - -.viewButton:hover { - 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); - } - - .tableHeader, - .tableRow { - grid-template-columns: 0.6fr 1.5fr 0.8fr 1fr 0.8fr 0.6fr 0.5fr; - } -} - -@media (max-width: 768px) { - .statsGrid { - grid-template-columns: 1fr; - } - - .tableHeader, - .tableRow { - grid-template-columns: 0.6fr 1.2fr 0.6fr 0.8fr 0.6fr 0.5fr 0.4fr; - gap: 0.5rem; - padding: 0.75rem 1rem; - } - - .headerCell, - .cell { - font-size: 0.75rem; - } -} +.container { + padding: 2rem; +} + +.maxWidth { + max-width: 96rem; +} + +.header { + margin-bottom: 2rem; +} + +.headerFlex { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.headerIcon { + color: rgb(37, 99, 235); + font-size: 1.75rem; +} + +.title { + font-size: 2.25rem; + font-weight: 700; + color: rgb(17, 24, 39); + line-height: 1.1; +} + +.subtitle { + color: rgb(75, 85, 99); + margin-bottom: 2rem; + font-size: 1.125rem; + line-height: 1.6; +} + +.statsGrid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1rem; + margin-bottom: 2rem; +} + +.statCard { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.25rem; +} + +.statLabel { + color: rgb(75, 85, 99); + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.statValue { + font-size: 1.875rem; + font-weight: bold; + color: rgb(17, 24, 39); +} + +.statIcon { + margin-top: 0.75rem; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; +} + +.statIconBlue { + background-color: rgb(219, 234, 254); + color: rgb(37, 99, 235); +} + +.statIconGreen { + background-color: rgb(220, 252, 231); + color: rgb(34, 197, 94); +} + +.filtersBox { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + padding: 1.5rem; + margin-bottom: 1.5rem; + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.filterLabel { + display: flex; + align-items: center; + gap: 0.5rem; + color: rgb(55, 65, 81); + font-weight: 500; + font-size: 0.875rem; +} + +.filterIcon { + font-size: 1rem; +} + +.select { + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 0.75rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; +} + +.select:focus { + outline: none; + box-shadow: 0 0 0 2px rgb(59, 130, 246); +} + +.exportWrapper { + margin-left: auto; +} + +.exportButton { + display: flex; + align-items: center; + gap: 0.5rem; + border: 1px solid rgb(209, 213, 219); + border-radius: 0.5rem; + font-size: 0.875rem; + padding: 0.375rem 1rem; + color: rgb(55, 65, 81); + background-color: white; + cursor: pointer; + transition: background-color 0.2s; +} + +.exportButton:hover { + background-color: rgb(249, 250, 251); +} + +.resultsInfo { + color: rgb(75, 85, 99); + margin-bottom: 1rem; + font-size: 0.875rem; +} + +.resultsCount { + font-weight: 600; +} + +.table { + background-color: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 0.75rem; + overflow: auto; + -webkit-overflow-scrolling: touch; +} + +.tableHeader { + display: grid; + grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; + gap: 1rem; + background-color: rgb(249, 250, 251); + border-bottom: 1px solid rgb(229, 231, 235); + padding: 0.75rem 1.5rem; +} + +.headerCell { + font-size: 0.875rem; + font-weight: 600; + color: rgb(55, 65, 81); +} + +.tableRow { + display: grid; + grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; + gap: 1rem; + padding: 1rem 1.5rem; + align-items: center; +} + +.tableRow:hover { + background-color: rgb(249, 250, 251); + transition: background-color 0.2s; +} + +.tableRowBorder { + border-bottom: 1px solid rgb(229, 231, 235); +} + +.statusBadge { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; +} + +.statusBadgeNew { + background-color: rgb(220, 252, 231); + color: rgb(22, 163, 74); +} + +.statusBadgeUpdated { + background-color: rgb(219, 234, 254); + color: rgb(37, 99, 235); +} + +.titleCell { + font-weight: 500; + color: rgb(17, 24, 39); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.cell { + font-size: 0.875rem; + color: rgb(75, 85, 99); +} + +.cellTruncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.viewButton { + background-color: rgb(37, 99, 235); + color: white; + font-size: 0.75rem; + font-weight: 500; + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + border: none; + cursor: pointer; + transition: background-color 0.2s; +} + +.viewButton:hover { + background-color: rgb(29, 78, 216); +} + +@media (max-width: 1024px) { + .statsGrid { + grid-template-columns: repeat(2, 1fr); + } + + .tableHeader, + .tableRow { + grid-template-columns: 0.6fr 1.5fr 0.8fr 1fr 0.8fr 0.6fr 0.5fr; + } +} + +@media (max-width: 768px) { + .statsGrid { + grid-template-columns: 1fr; + } + + .tableHeader, + .tableRow { + grid-template-columns: 0.6fr 1.2fr 0.6fr 0.8fr 0.6fr 0.5fr 0.4fr; + gap: 0.5rem; + padding: 0.75rem 1rem; + } + + .headerCell, + .cell { + font-size: 0.75rem; + } +}