Rétablit les fins de ligne CRLF sur les fichiers du commit précédent
Build & Deploy / build (push) Successful in 27s

Le script d'édition du commit précédent a réécrit ces cinq fichiers en LF alors
que le dépôt les stocke en CRLF, ce qui gonflait le diff au fichier entier et
aurait rendu les prochains merges pénibles. Contenu strictement identique, seules
les fins de ligne reviennent à la convention du dépôt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:40:30 +02:00
co-authored by Claude Opus 5
parent f170da8997
commit b86bd6fbc2
5 changed files with 1175 additions and 1175 deletions
+258 -258
View File
@@ -1,259 +1,259 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import {
Filter, Filter,
ArrowUpDown, ArrowUpDown,
File as FileIcon, File as FileIcon,
} 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 Pagination from "../components/Pagination";
import { getDocuments } from "../services/theapi"; import { getDocuments } from "../services/theapi";
type ApiDocument = { type ApiDocument = {
title: string; title: string;
ingredient: string; ingredient: string;
source: string; source: string;
type: string; type: string;
date?: string | null; date?: string | null;
pdf_url: string; pdf_url: string;
bold?: boolean; bold?: boolean;
}; };
const DEFAULT_ITEMS_PER_PAGE = 8; const DEFAULT_ITEMS_PER_PAGE = 8;
export default function DocumentsPage() { export default function DocumentsPage() {
const [documents, setDocuments] = useState<ApiDocument[]>([]); const [documents, setDocuments] = useState<ApiDocument[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE);
const [sourceFilter, setSourceFilter] = useState("All Sources"); const [sourceFilter, setSourceFilter] = useState("All Sources");
const [typeFilter, setTypeFilter] = useState("All Types"); const [typeFilter, setTypeFilter] = useState("All Types");
const [dateFilter, setDateFilter] = useState("All Dates"); const [dateFilter, setDateFilter] = useState("All Dates");
const [sortBy, setSortBy] = useState("Date (Newest)"); const [sortBy, setSortBy] = useState("Date (Newest)");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
useEffect(() => { useEffect(() => {
getDocuments() getDocuments()
.then((data) => { .then((data) => {
setDocuments(Array.isArray(data) ? data : []); setDocuments(Array.isArray(data) ? data : []);
}) })
.catch((err) => { .catch((err) => {
console.error("API Error:", err); console.error("API Error:", err);
setDocuments([]); setDocuments([]);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const allSources = useMemo( const allSources = useMemo(
() => ["All Sources", ...new Set(documents.map((d) => d.source))], () => ["All Sources", ...new Set(documents.map((d) => d.source))],
[documents] [documents]
); );
const allTypes = useMemo( const allTypes = useMemo(
() => ["All Types", ...new Set(documents.map((d) => d.type))], () => ["All Types", ...new Set(documents.map((d) => d.type))],
[documents] [documents]
); );
const allDates = useMemo( const allDates = useMemo(
() => [ () => [
"All Dates", "All Dates",
...new Set( ...new Set(
documents.map((d) => String(d.date ?? "").slice(0, 4)) documents.map((d) => String(d.date ?? "").slice(0, 4))
), ),
], ],
[documents] [documents]
); );
const filtered = useMemo(() => { const filtered = useMemo(() => {
return documents return documents
.filter((d) => { .filter((d) => {
const sourceMatch = const sourceMatch =
sourceFilter === "All Sources" || d.source === sourceFilter; sourceFilter === "All Sources" || d.source === sourceFilter;
const typeMatch = const typeMatch =
typeFilter === "All Types" || d.type === typeFilter; typeFilter === "All Types" || d.type === typeFilter;
const safeDate = String(d.date ?? ""); const safeDate = String(d.date ?? "");
const dateMatch = const dateMatch =
dateFilter === "All Dates" || dateFilter === "All Dates" ||
safeDate.startsWith(dateFilter); safeDate.startsWith(dateFilter);
return sourceMatch && typeMatch && dateMatch; return sourceMatch && typeMatch && dateMatch;
}) })
.sort((a, b) => { .sort((a, b) => {
const dateA = String(a.date ?? ""); const dateA = String(a.date ?? "");
const dateB = String(b.date ?? ""); const dateB = String(b.date ?? "");
const titleA = String(a.title ?? ""); const titleA = String(a.title ?? "");
const titleB = String(b.title ?? ""); const titleB = String(b.title ?? "");
if (sortBy === "Date (Newest)") return dateB.localeCompare(dateA); if (sortBy === "Date (Newest)") return dateB.localeCompare(dateA);
if (sortBy === "Date (Oldest)") return dateA.localeCompare(dateB); if (sortBy === "Date (Oldest)") return dateA.localeCompare(dateB);
if (sortBy === "Title (A-Z)") return titleA.localeCompare(titleB); if (sortBy === "Title (A-Z)") return titleA.localeCompare(titleB);
if (sortBy === "Title (Z-A)") return titleB.localeCompare(titleA); if (sortBy === "Title (Z-A)") return titleB.localeCompare(titleA);
return 0; return 0;
}); });
}, [documents, sourceFilter, typeFilter, dateFilter, sortBy]); }, [documents, sourceFilter, typeFilter, dateFilter, sortBy]);
const totalPages = Math.max( const totalPages = Math.max(
1, 1,
Math.ceil(filtered.length / itemsPerPage) Math.ceil(filtered.length / itemsPerPage)
); );
const safePage = Math.min(currentPage, totalPages); const safePage = Math.min(currentPage, totalPages);
const paginated = filtered.slice( const paginated = filtered.slice(
(safePage - 1) * itemsPerPage, (safePage - 1) * itemsPerPage,
safePage * itemsPerPage safePage * itemsPerPage
); );
// adjust items per page based on viewport width // adjust items per page based on viewport width
useEffect(() => { useEffect(() => {
const apply = () => { const apply = () => {
const w = window.innerWidth; const w = window.innerWidth;
if (w < 640) setItemsPerPage(3); if (w < 640) setItemsPerPage(3);
else if (w < 900) setItemsPerPage(6); else if (w < 900) setItemsPerPage(6);
else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE);
}; };
apply(); apply();
window.addEventListener('resize', apply); window.addEventListener('resize', apply);
return () => window.removeEventListener('resize', apply); return () => window.removeEventListener('resize', apply);
}, []); }, []);
if (loading) { if (loading) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<p>Loading documents...</p> <p>Loading documents...</p>
</div> </div>
); );
} }
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* HEADER */} {/* HEADER */}
<div className={styles.header}> <div className={styles.header}>
<h2 className={styles.title}>Document Database</h2> <h2 className={styles.title}>Document Database</h2>
<p className={styles.description}> <p className={styles.description}>
Complete collection of regulatory monitoring documents Complete collection of regulatory monitoring documents
</p> </p>
</div> </div>
{/* FILTERS */} {/* FILTERS */}
<div className={styles.filtersBox}> <div className={styles.filtersBox}>
<div className={styles.filterLabel}> <div className={styles.filterLabel}>
<Filter size={16} /> <Filter size={16} />
<span>Filters:</span> <span>Filters:</span>
</div> </div>
<select <select
value={sourceFilter} value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value)} onChange={(e) => setSourceFilter(e.target.value)}
className={styles.select} className={styles.select}
> >
{allSources.map((s) => ( {allSources.map((s) => (
<option key={s} value={s}> <option key={s} value={s}>
{s} {s}
</option> </option>
))} ))}
</select> </select>
<select <select
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} onChange={(e) => setTypeFilter(e.target.value)}
className={styles.select} className={styles.select}
> >
{allTypes.map((t) => ( {allTypes.map((t) => (
<option key={t} value={t}> <option key={t} value={t}>
{t} {t}
</option> </option>
))} ))}
</select> </select>
<select <select
value={dateFilter} value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)} onChange={(e) => setDateFilter(e.target.value)}
className={styles.select} className={styles.select}
> >
{allDates.map((d) => ( {allDates.map((d) => (
<option key={d} value={d}> <option key={d} value={d}>
{d} {d}
</option> </option>
))} ))}
</select> </select>
<div className={styles.sortWrapper}> <div className={styles.sortWrapper}>
<ArrowUpDown size={16} /> <ArrowUpDown size={16} />
<select <select
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value)} onChange={(e) => setSortBy(e.target.value)}
className={styles.select} className={styles.select}
> >
<option value="Date (Newest)">Date (Newest)</option> <option value="Date (Newest)">Date (Newest)</option>
<option value="Date (Oldest)">Date (Oldest)</option> <option value="Date (Oldest)">Date (Oldest)</option>
<option value="Title (A-Z)">Title (A-Z)</option> <option value="Title (A-Z)">Title (A-Z)</option>
<option value="Title (Z-A)">Title (Z-A)</option> <option value="Title (Z-A)">Title (Z-A)</option>
</select> </select>
</div> </div>
</div> </div>
{/* STATS */} {/* STATS */}
<div className={styles.statsRow}> <div className={styles.statsRow}>
<p className={styles.statsText}> <p className={styles.statsText}>
Total Documents: Total Documents:
<span className={styles.statsNumber}> <span className={styles.statsNumber}>
{" "} {" "}
{filtered.length} {filtered.length}
</span> </span>
</p> </p>
</div> </div>
{/* TABLE */} {/* TABLE */}
<div className={styles.table}> <div className={styles.table}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<span>Title</span> <span>Title</span>
<span>Ingredient</span> <span>Ingredient</span>
<span>Source</span> <span>Source</span>
<span>Type</span> <span>Type</span>
<span>Date</span> <span>Date</span>
<span>PDF</span> <span>PDF</span>
</div> </div>
{paginated.map((doc, index) => ( {paginated.map((doc, index) => (
<div <div
key={`${doc.title}-${doc.source}-${doc.date ?? index}`} key={`${doc.title}-${doc.source}-${doc.date ?? index}`}
className={styles.tableRow} className={styles.tableRow}
> >
<div className={styles.titleCell}> <div className={styles.titleCell}>
<FileIcon size={16} /> <FileIcon size={16} />
<span>{doc.title}</span> <span>{doc.title}</span>
</div> </div>
<span>{doc.ingredient}</span> <span>{doc.ingredient}</span>
<span>{doc.source}</span> <span>{doc.source}</span>
<span>{doc.type}</span> <span>{doc.type}</span>
<span>{doc.date}</span> <span>{doc.date}</span>
<div> <div>
<a <a
href={doc.pdf_url} href={doc.pdf_url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
<button className={styles.openButton}> <button className={styles.openButton}>
Open Open
</button> </button>
</a> </a>
</div> </div>
</div> </div>
))} ))}
</div> </div>
<Pagination <Pagination
currentPage={safePage} currentPage={safePage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
/> />
</div> </div>
</div> </div>
); );
} }
+191 -191
View File
@@ -1,192 +1,192 @@
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 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() {
const [documents, setDocuments] = useState<ApiDocument[]>([]); const [documents, setDocuments] = useState<ApiDocument[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [sourceFilter, setSourceFilter] = useState("All Sources"); const [sourceFilter, setSourceFilter] = useState("All Sources");
const [typeFilter, setTypeFilter] = useState("All Types"); const [typeFilter, setTypeFilter] = useState("All Types");
const [dateFilter, setDateFilter] = useState("All Dates"); const [dateFilter, setDateFilter] = useState("All Dates");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(8); const [itemsPerPage, setItemsPerPage] = useState(8);
// FETCH API // FETCH API
useEffect(() => { useEffect(() => {
getDocuments() getDocuments()
.then((data) => setDocuments(Array.isArray(data) ? data : [])) .then((data) => setDocuments(Array.isArray(data) ? data : []))
.catch((err) => { .catch((err) => {
console.error("API ERROR:", err); console.error("API ERROR:", err);
setDocuments([]); setDocuments([]);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
// responsive items per page // responsive items per page
useEffect(() => { useEffect(() => {
const apply = () => { const apply = () => {
const w = window.innerWidth; const w = window.innerWidth;
if (w < 640) setItemsPerPage(3); if (w < 640) setItemsPerPage(3);
else if (w < 900) setItemsPerPage(6); else if (w < 900) setItemsPerPage(6);
else setItemsPerPage(8); else setItemsPerPage(8);
}; };
apply(); apply();
window.addEventListener('resize', apply); window.addEventListener('resize', apply);
return () => window.removeEventListener('resize', apply); return () => window.removeEventListener('resize', apply);
}, []); }, []);
// FILTER OPTIONS // FILTER OPTIONS
const allSources = useMemo(() => [ const allSources = useMemo(() => [
"All Sources", "All Sources",
...new Set(documents.map((d) => d.source)), ...new Set(documents.map((d) => d.source)),
], [documents]); ], [documents]);
const allTypes = useMemo(() => [ const allTypes = useMemo(() => [
"All Types", "All Types",
...new Set(documents.map((d) => d.type)), ...new Set(documents.map((d) => d.type)),
], [documents]); ], [documents]);
const allDates = useMemo(() => [ const allDates = useMemo(() => [
"All Dates", "All Dates",
...new Set(documents.map((d) => String(d.date ?? "").slice(0, 4))).values(), ...new Set(documents.map((d) => String(d.date ?? "").slice(0, 4))).values(),
], [documents]); ], [documents]);
// FILTERED RESULTS // FILTERED RESULTS
const filtered = useMemo(() => { const filtered = useMemo(() => {
return documents return documents
.filter((d) => { .filter((d) => {
const queryMatch = const queryMatch =
searchQuery === "" || searchQuery === "" ||
d.ingredient.toLowerCase().includes(searchQuery.toLowerCase()) || d.ingredient.toLowerCase().includes(searchQuery.toLowerCase()) ||
d.title.toLowerCase().includes(searchQuery.toLowerCase()); d.title.toLowerCase().includes(searchQuery.toLowerCase());
const sourceMatch = sourceFilter === "All Sources" || d.source === sourceFilter; const sourceMatch = sourceFilter === "All Sources" || d.source === sourceFilter;
const typeMatch = typeFilter === "All Types" || d.type === typeFilter; const typeMatch = typeFilter === "All Types" || d.type === typeFilter;
const safeDate = String(d.date ?? ""); const safeDate = String(d.date ?? "");
const dateMatch = dateFilter === "All Dates" || safeDate.startsWith(dateFilter); const dateMatch = dateFilter === "All Dates" || safeDate.startsWith(dateFilter);
return queryMatch && sourceMatch && typeMatch && dateMatch; return queryMatch && sourceMatch && typeMatch && dateMatch;
}) })
.sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? ""))); .sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? "")));
}, [documents, searchQuery, sourceFilter, typeFilter, dateFilter]); }, [documents, searchQuery, sourceFilter, typeFilter, dateFilter]);
if (loading) { if (loading) {
return <div className={styles.container}><p>Loading...</p></div>; return <div className={styles.container}><p>Loading...</p></div>;
} }
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* HEADER */} {/* HEADER */}
<div className={styles.header}> <div className={styles.header}>
<h1 className={styles.title}>Ingredient Search</h1> <h1 className={styles.title}>Ingredient Search</h1>
<p className={styles.subtitle}>Search cosmetic ingredients and regulatory documents</p> <p className={styles.subtitle}>Search cosmetic ingredients and regulatory documents</p>
</div> </div>
{/* SEARCH */} {/* SEARCH */}
<div className={styles.searchBarWrapper}> <div className={styles.searchBarWrapper}>
<div className={styles.searchBar}> <div className={styles.searchBar}>
<input <input
className={styles.searchInput} className={styles.searchInput}
type="text" type="text"
placeholder="Search ingredient..." placeholder="Search ingredient..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
/> />
<Search size={20} className={styles.searchIcon} /> <Search size={20} className={styles.searchIcon} />
</div> </div>
</div> </div>
{/* FILTERS */} {/* FILTERS */}
<div className={styles.filtersBox}> <div className={styles.filtersBox}>
<div className={styles.filterHeader}> <div className={styles.filterHeader}>
<Filter size={16} className={styles.filterIcon} /> <Filter size={16} className={styles.filterIcon} />
<span>Filters</span> <span>Filters</span>
</div> </div>
<div className={styles.filterControls}> <div className={styles.filterControls}>
<select className={styles.select} value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}> <select className={styles.select} value={sourceFilter} onChange={(e) => setSourceFilter(e.target.value)}>
{allSources.map((s) => <option key={s} value={s}>{s}</option>)} {allSources.map((s) => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select className={styles.select} value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}> <select className={styles.select} value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)}>
{allTypes.map((t) => <option key={t} value={t}>{t}</option>)} {allTypes.map((t) => <option key={t} value={t}>{t}</option>)}
</select> </select>
<select className={styles.select} value={dateFilter} onChange={(e) => setDateFilter(e.target.value)}> <select className={styles.select} value={dateFilter} onChange={(e) => setDateFilter(e.target.value)}>
{allDates.map((d) => <option key={d} value={d}>{d || "Unknown"}</option>)} {allDates.map((d) => <option key={d} value={d}>{d || "Unknown"}</option>)}
</select> </select>
</div> </div>
</div> </div>
{/* RESULTS */} {/* RESULTS */}
<div className={styles.resultsInfo}> <div className={styles.resultsInfo}>
<span className={styles.resultsCount}>{filtered.length}</span> results found <span className={styles.resultsCount}>{filtered.length}</span> results found
</div> </div>
{/* DOCUMENTS */} {/* DOCUMENTS */}
<div className={styles.documentsList}> <div className={styles.documentsList}>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<p className={styles.emptyMessage}>No documents found.</p> <p className={styles.emptyMessage}>No documents found.</p>
</div> </div>
) : ( ) : (
(() => { (() => {
const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage));
const safePage = Math.min(currentPage, totalPages); const safePage = Math.min(currentPage, totalPages);
const paginated = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage); const paginated = filtered.slice((safePage - 1) * itemsPerPage, safePage * itemsPerPage);
return ( return (
<> <>
{paginated.map((doc) => ( {paginated.map((doc) => (
<div key={doc.id} className={styles.documentCard}> <div key={doc.id} className={styles.documentCard}>
<div className={styles.documentCardFlex}> <div className={styles.documentCardFlex}>
<div className={styles.documentContent}> <div className={styles.documentContent}>
<h3 className={styles.documentTitle}>{doc.title}</h3> <h3 className={styles.documentTitle}>{doc.title}</h3>
<div className={styles.documentMeta}> <div className={styles.documentMeta}>
<div className={styles.metaItem}> <div className={styles.metaItem}>
<span className={styles.metaLabel}>Ingredient</span> <span className={styles.metaLabel}>Ingredient</span>
<span className={styles.metaValue}>{doc.ingredient}</span> <span className={styles.metaValue}>{doc.ingredient}</span>
</div> </div>
<div className={styles.metaItem}> <div className={styles.metaItem}>
<span className={styles.metaLabel}>Source</span> <span className={styles.metaLabel}>Source</span>
<span className={styles.metaValue}>{doc.source}</span> <span className={styles.metaValue}>{doc.source}</span>
</div> </div>
<div className={styles.metaItem}> <div className={styles.metaItem}>
<span className={styles.metaLabel}>Type</span> <span className={styles.metaLabel}>Type</span>
<span className={styles.metaValue}>{doc.type}</span> <span className={styles.metaValue}>{doc.type}</span>
</div> </div>
<div className={styles.metaItem}> <div className={styles.metaItem}>
<span className={styles.metaLabel}>Date</span> <span className={styles.metaLabel}>Date</span>
<span className={styles.metaValue}>{doc.date ?? "—"}</span> <span className={styles.metaValue}>{doc.date ?? "—"}</span>
</div> </div>
</div> </div>
</div> </div>
{doc.pdf_url && ( {doc.pdf_url && (
<button <button
className={styles.openButton} className={styles.openButton}
onClick={() => openPdf(doc.pdf_url)} onClick={() => openPdf(doc.pdf_url)}
> >
Open PDF Open PDF
</button> </button>
)} )}
</div> </div>
</div> </div>
))} ))}
<Pagination <Pagination
currentPage={safePage} currentPage={safePage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
/> />
</> </>
); );
})() })()
)} )}
</div> </div>
</div> </div>
</div> </div>
); );
} }
+211 -211
View File
@@ -1,212 +1,212 @@
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 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;
export default function RecentUpdatesPage() { export default function RecentUpdatesPage() {
const [updates, setUpdates] = useState<ApiDocument[]>([]); const [updates, setUpdates] = useState<ApiDocument[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [sourceFilter, setSourceFilter] = useState("All Sources"); const [sourceFilter, setSourceFilter] = useState("All Sources");
const [typeFilter, setTypeFilter] = useState("All Types"); const [typeFilter, setTypeFilter] = useState("All Types");
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE); const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE);
useEffect(() => { useEffect(() => {
getDocuments() getDocuments()
.then((data) => setUpdates(Array.isArray(data) ? data : [])) .then((data) => setUpdates(Array.isArray(data) ? data : []))
.catch((err) => { .catch((err) => {
console.error("API ERROR:", err); console.error("API ERROR:", err);
setUpdates([]); setUpdates([]);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const allSources = useMemo( const allSources = useMemo(
() => ["All Sources", ...new Set(updates.map((u) => u.source))], () => ["All Sources", ...new Set(updates.map((u) => u.source))],
[updates] [updates]
); );
const allTypes = useMemo( const allTypes = useMemo(
() => ["All Types", ...new Set(updates.map((u) => u.type))], () => ["All Types", ...new Set(updates.map((u) => u.type))],
[updates] [updates]
); );
const filtered = useMemo(() => { const filtered = useMemo(() => {
return updates return updates
.filter((u) => { .filter((u) => {
const sourceMatch = const sourceMatch =
sourceFilter === "All Sources" || u.source === sourceFilter; sourceFilter === "All Sources" || u.source === sourceFilter;
const typeMatch = const typeMatch =
typeFilter === "All Types" || u.type === typeFilter; typeFilter === "All Types" || u.type === typeFilter;
return sourceMatch && typeMatch; return sourceMatch && typeMatch;
}) })
.sort((a, b) => .sort((a, b) =>
String(b.date ?? "").localeCompare(String(a.date ?? "")) String(b.date ?? "").localeCompare(String(a.date ?? ""))
); );
}, [updates, sourceFilter, typeFilter]); }, [updates, sourceFilter, typeFilter]);
const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage)); const totalPages = Math.max(1, Math.ceil(filtered.length / itemsPerPage));
const safePage = Math.min(currentPage, totalPages); const safePage = Math.min(currentPage, totalPages);
const paginated = filtered.slice( const paginated = filtered.slice(
(safePage - 1) * itemsPerPage, (safePage - 1) * itemsPerPage,
safePage * itemsPerPage safePage * itemsPerPage
); );
useEffect(() => { useEffect(() => {
const apply = () => { const apply = () => {
const w = window.innerWidth; const w = window.innerWidth;
if (w < 640) setItemsPerPage(3); if (w < 640) setItemsPerPage(3);
else if (w < 900) setItemsPerPage(6); else if (w < 900) setItemsPerPage(6);
else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE); else setItemsPerPage(DEFAULT_ITEMS_PER_PAGE);
}; };
apply(); apply();
window.addEventListener('resize', apply); window.addEventListener('resize', apply);
return () => window.removeEventListener('resize', apply); return () => window.removeEventListener('resize', apply);
}, []); }, []);
if (loading) { if (loading) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<p>Loading updates...</p> <p>Loading updates...</p>
</div> </div>
); );
} }
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* HEADER */} {/* HEADER */}
<div className={styles.header}> <div className={styles.header}>
<div className={styles.headerFlex}> <div className={styles.headerFlex}>
<TrendingUp size={28} className={styles.headerIcon} /> <TrendingUp size={28} className={styles.headerIcon} />
<h1 className={styles.title}>Recent Updates</h1> <h1 className={styles.title}>Recent Updates</h1>
</div> </div>
<p className={styles.subtitle}> <p className={styles.subtitle}>
Latest cosmetic regulatory updates and monitoring activity Latest cosmetic regulatory updates and monitoring activity
</p> </p>
</div> </div>
{/* STATS */} {/* STATS */}
<div className={styles.statsGrid}> <div className={styles.statsGrid}>
<div className={styles.statCard}> <div className={styles.statCard}>
<p className={styles.statLabel}>Total Updates</p> <p className={styles.statLabel}>Total Updates</p>
<h2 className={styles.statValue}>{updates.length}</h2> <h2 className={styles.statValue}>{updates.length}</h2>
<div className={`${styles.statIcon} ${styles.statIconBlue}`}> <div className={`${styles.statIcon} ${styles.statIconBlue}`}>
<TrendingUp size={18} /> <TrendingUp size={18} />
</div> </div>
</div> </div>
<div className={styles.statCard}> <div className={styles.statCard}>
<p className={styles.statLabel}>Sources</p> <p className={styles.statLabel}>Sources</p>
<h2 className={styles.statValue}>{allSources.length - 1}</h2> <h2 className={styles.statValue}>{allSources.length - 1}</h2>
<div className={`${styles.statIcon} ${styles.statIconGreen}`}> <div className={`${styles.statIcon} ${styles.statIconGreen}`}>
<Filter size={18} /> <Filter size={18} />
</div> </div>
</div> </div>
</div> </div>
{/* FILTERS */} {/* FILTERS */}
<div className={styles.filtersBox}> <div className={styles.filtersBox}>
<div className={styles.filterLabel}> <div className={styles.filterLabel}>
<Filter size={16} /> <Filter size={16} />
<span>Filters</span> <span>Filters</span>
</div> </div>
<select <select
className={styles.select} className={styles.select}
value={sourceFilter} value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value)} onChange={(e) => setSourceFilter(e.target.value)}
> >
{allSources.map((s) => ( {allSources.map((s) => (
<option key={s} value={s}> <option key={s} value={s}>
{s} {s}
</option> </option>
))} ))}
</select> </select>
<select <select
className={styles.select} className={styles.select}
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} onChange={(e) => setTypeFilter(e.target.value)}
> >
{allTypes.map((t) => ( {allTypes.map((t) => (
<option key={t} value={t}> <option key={t} value={t}>
{t} {t}
</option> </option>
))} ))}
</select> </select>
</div> </div>
{/* RESULTS */} {/* RESULTS */}
<div className={styles.resultsInfo}> <div className={styles.resultsInfo}>
<span className={styles.resultsCount}>{filtered.length}</span>{" "} <span className={styles.resultsCount}>{filtered.length}</span>{" "}
updates found updates found
</div> </div>
{/* TABLE */} {/* TABLE */}
<div className={styles.table}> <div className={styles.table}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<span className={styles.headerCell}>Status</span> <span className={styles.headerCell}>Status</span>
<span className={styles.headerCell}>Title</span> <span className={styles.headerCell}>Title</span>
<span className={styles.headerCell}>Ingredient</span> <span className={styles.headerCell}>Ingredient</span>
<span className={styles.headerCell}>Source</span> <span className={styles.headerCell}>Source</span>
<span className={styles.headerCell}>Type</span> <span className={styles.headerCell}>Type</span>
<span className={styles.headerCell}>Date</span> <span className={styles.headerCell}>Date</span>
<span className={styles.headerCell}>View</span> <span className={styles.headerCell}>View</span>
</div> </div>
{paginated.map((u, index) => ( {paginated.map((u, index) => (
<div <div
key={u.id} key={u.id}
className={`${styles.tableRow} ${ className={`${styles.tableRow} ${
index < paginated.length - 1 index < paginated.length - 1
? styles.tableRowBorder ? styles.tableRowBorder
: "" : ""
}`} }`}
> >
<div> <div>
<span <span
className={`${styles.statusBadge} ${styles.statusBadgeNew}`} className={`${styles.statusBadge} ${styles.statusBadgeNew}`}
> >
New New
</span> </span>
</div> </div>
<div className={`${styles.titleCell} ${styles.cellTruncate}`}> <div className={`${styles.titleCell} ${styles.cellTruncate}`}>
{u.title} {u.title}
</div> </div>
<div className={styles.cell}>{u.ingredient}</div> <div className={styles.cell}>{u.ingredient}</div>
<div className={styles.cell}>{u.source}</div> <div className={styles.cell}>{u.source}</div>
<div className={styles.cell}>{u.type}</div> <div className={styles.cell}>{u.type}</div>
<div className={styles.cell}>{u.date ?? "—"}</div> <div className={styles.cell}>{u.date ?? "—"}</div>
<div> <div>
{u.pdf_url && ( {u.pdf_url && (
<button <button
className={styles.viewButton} className={styles.viewButton}
onClick={() => openPdf(u.pdf_url)} onClick={() => openPdf(u.pdf_url)}
> >
<Eye size={14} /> <Eye size={14} />
</button> </button>
)} )}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<Pagination <Pagination
currentPage={safePage} currentPage={safePage}
totalPages={totalPages} totalPages={totalPages}
onPageChange={setCurrentPage} onPageChange={setCurrentPage}
/> />
</div> </div>
</div> </div>
); );
} }
+237 -237
View File
@@ -1,237 +1,237 @@
.container { .container {
padding: 2rem; padding: 2rem;
} }
.maxWidth { .maxWidth {
max-width: 80rem; max-width: 80rem;
} }
.header { .header {
margin-bottom: 1.75rem; margin-bottom: 1.75rem;
} }
.title { .title {
font-size: 2.25rem; font-size: 2.25rem;
font-weight: 700; font-weight: 700;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
line-height: 1.1; line-height: 1.1;
} }
.description { .description {
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.6; line-height: 1.6;
} }
.filtersBox { .filtersBox {
background-color: white; background-color: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.25rem; padding: 1.25rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.filterLabel { .filterLabel {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
font-weight: 500; font-weight: 500;
font-size: 0.875rem; font-size: 0.875rem;
} }
.filterIcon { .filterIcon {
font-size: 1rem; font-size: 1rem;
} }
.select { .select {
border: 1px solid rgb(209, 213, 219); border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
background-color: white; background-color: white;
cursor: pointer; cursor: pointer;
} }
.select:focus { .select:focus {
outline: none; outline: none;
box-shadow: 0 0 0 2px rgb(59, 130, 246); box-shadow: 0 0 0 2px rgb(59, 130, 246);
} }
.sortWrapper { .sortWrapper {
margin-left: auto; margin-left: auto;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
} }
.sortIcon { .sortIcon {
color: rgb(107, 114, 128); color: rgb(107, 114, 128);
} }
.statsRow { .statsRow {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
.statsText { .statsText {
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
} }
.statsNumber { .statsNumber {
font-weight: 600; font-weight: 600;
} }
.exportButton { .exportButton {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
border: 1px solid rgb(209, 213, 219); border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.375rem 1rem; padding: 0.375rem 1rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
background-color: white; background-color: white;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.exportButton:hover { .exportButton:hover {
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
} }
.table { .table {
background-color: white; background-color: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.tableHeader { .tableHeader {
display: grid; display: grid;
grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr;
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
border-bottom: 1px solid rgb(229, 231, 235); border-bottom: 1px solid rgb(229, 231, 235);
padding: 0.75rem 1.25rem; padding: 0.75rem 1.25rem;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.tableRow { .tableRow {
display: grid; display: grid;
grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr; grid-template-columns: minmax(180px,2fr) 1.2fr 1fr 1.2fr 0.8fr 0.6fr;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
align-items: center; align-items: center;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.tableRow span, .tableRow span,
.titleCell span, .titleCell span,
.titleCell { .titleCell {
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.headerCell { .headerCell {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
} }
.tableRow { .tableRow {
display: grid; display: grid;
grid-template-columns: 2fr 1.2fr 1fr 1.2fr 0.8fr 0.6fr; grid-template-columns: 2fr 1.2fr 1fr 1.2fr 0.8fr 0.6fr;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
align-items: center; align-items: center;
} }
.tableRow:hover { .tableRow:hover {
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.tableRowBorder { .tableRowBorder {
border-bottom: 1px solid rgb(229, 231, 235); border-bottom: 1px solid rgb(229, 231, 235);
} }
.titleCell { .titleCell {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 0.625rem; gap: 0.625rem;
} }
.titleIcon { .titleIcon {
color: rgb(156, 163, 175); color: rgb(156, 163, 175);
margin-top: 0.125rem; margin-top: 0.125rem;
flex-shrink: 0; flex-shrink: 0;
} }
.titleText { .titleText {
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(31, 41, 55); color: rgb(31, 41, 55);
line-height: 1.5; line-height: 1.5;
} }
.ingredientBadge { .ingredientBadge {
display: inline-block; display: inline-block;
font-size: 0.75rem; font-size: 0.75rem;
padding: 0.625rem; padding: 0.625rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid rgb(209, 213, 219); border: 1px solid rgb(209, 213, 219);
background-color: rgb(243, 244, 246); background-color: rgb(243, 244, 246);
} }
.ingredientBadgeBold { .ingredientBadgeBold {
font-weight: 600; font-weight: 600;
border-color: rgb(209, 213, 219); border-color: rgb(209, 213, 219);
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.ingredientBadgeNormal { .ingredientBadgeNormal {
border-color: rgb(229, 231, 235); border-color: rgb(229, 231, 235);
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
} }
.cell { .cell {
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.cellWhitespace { .cellWhitespace {
white-space: nowrap; white-space: nowrap;
} }
.openButton { .openButton {
background-color: rgb(37, 99, 235); background-color: rgb(37, 99, 235);
color: white; color: white;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
padding: 0.375rem 1rem; padding: 0.375rem 1rem;
border-radius: 0.5rem; border-radius: 0.5rem;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.openButton:hover { .openButton:hover {
background-color: rgb(29, 78, 216); background-color: rgb(29, 78, 216);
} }
/* Mobile: make each table row a card */ /* Mobile: make each table row a card */
@media (max-width: 640px) { @media (max-width: 640px) {
.tableHeader { display: none; } .tableHeader { display: none; }
.tableRow { display: block; padding: 12px; border-bottom: 1px solid rgb(229,231,235); } .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 } .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) } .tableRow span { display:block; margin-top:6px; color: rgb(17,24,39) }
.openButton { width:100%; display:block } .openButton { width:100%; display:block }
} }
+278 -278
View File
@@ -1,278 +1,278 @@
.container { .container {
padding: 2rem; padding: 2rem;
} }
.maxWidth { .maxWidth {
max-width: 96rem; max-width: 96rem;
} }
.header { .header {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.headerFlex { .headerFlex {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.headerIcon { .headerIcon {
color: rgb(37, 99, 235); color: rgb(37, 99, 235);
font-size: 1.75rem; font-size: 1.75rem;
} }
.title { .title {
font-size: 2.25rem; font-size: 2.25rem;
font-weight: 700; font-weight: 700;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
line-height: 1.1; line-height: 1.1;
} }
.subtitle { .subtitle {
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
margin-bottom: 2rem; margin-bottom: 2rem;
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.6; line-height: 1.6;
} }
.statsGrid { .statsGrid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 1rem; gap: 1rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.statCard { .statCard {
background-color: white; background-color: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.25rem; padding: 1.25rem;
} }
.statLabel { .statLabel {
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.statValue { .statValue {
font-size: 1.875rem; font-size: 1.875rem;
font-weight: bold; font-weight: bold;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
.statIcon { .statIcon {
margin-top: 0.75rem; margin-top: 0.75rem;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border-radius: 0.5rem; border-radius: 0.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.statIconBlue { .statIconBlue {
background-color: rgb(219, 234, 254); background-color: rgb(219, 234, 254);
color: rgb(37, 99, 235); color: rgb(37, 99, 235);
} }
.statIconGreen { .statIconGreen {
background-color: rgb(220, 252, 231); background-color: rgb(220, 252, 231);
color: rgb(34, 197, 94); color: rgb(34, 197, 94);
} }
.filtersBox { .filtersBox {
background-color: white; background-color: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
border-radius: 0.75rem; border-radius: 0.75rem;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
flex-wrap: wrap; flex-wrap: wrap;
} }
.filterLabel { .filterLabel {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
font-weight: 500; font-weight: 500;
font-size: 0.875rem; font-size: 0.875rem;
} }
.filterIcon { .filterIcon {
font-size: 1rem; font-size: 1rem;
} }
.select { .select {
border: 1px solid rgb(209, 213, 219); border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
background-color: white; background-color: white;
cursor: pointer; cursor: pointer;
} }
.select:focus { .select:focus {
outline: none; outline: none;
box-shadow: 0 0 0 2px rgb(59, 130, 246); box-shadow: 0 0 0 2px rgb(59, 130, 246);
} }
.exportWrapper { .exportWrapper {
margin-left: auto; margin-left: auto;
} }
.exportButton { .exportButton {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
border: 1px solid rgb(209, 213, 219); border: 1px solid rgb(209, 213, 219);
border-radius: 0.5rem; border-radius: 0.5rem;
font-size: 0.875rem; font-size: 0.875rem;
padding: 0.375rem 1rem; padding: 0.375rem 1rem;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
background-color: white; background-color: white;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.exportButton:hover { .exportButton:hover {
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
} }
.resultsInfo { .resultsInfo {
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 0.875rem; font-size: 0.875rem;
} }
.resultsCount { .resultsCount {
font-weight: 600; font-weight: 600;
} }
.table { .table {
background-color: white; background-color: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
border-radius: 0.75rem; border-radius: 0.75rem;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.tableHeader { .tableHeader {
display: grid; display: grid;
grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr;
gap: 1rem; gap: 1rem;
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
border-bottom: 1px solid rgb(229, 231, 235); border-bottom: 1px solid rgb(229, 231, 235);
padding: 0.75rem 1.5rem; padding: 0.75rem 1.5rem;
} }
.headerCell { .headerCell {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
} }
.tableRow { .tableRow {
display: grid; display: grid;
grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr; grid-template-columns: 0.8fr minmax(180px,2fr) 1fr 1.2fr 1fr 0.8fr 0.6fr;
gap: 1rem; gap: 1rem;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
align-items: center; align-items: center;
} }
.tableRow:hover { .tableRow:hover {
background-color: rgb(249, 250, 251); background-color: rgb(249, 250, 251);
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.tableRowBorder { .tableRowBorder {
border-bottom: 1px solid rgb(229, 231, 235); border-bottom: 1px solid rgb(229, 231, 235);
} }
.statusBadge { .statusBadge {
display: inline-block; display: inline-block;
padding: 0.25rem 0.75rem; padding: 0.25rem 0.75rem;
border-radius: 9999px; border-radius: 9999px;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
} }
.statusBadgeNew { .statusBadgeNew {
background-color: rgb(220, 252, 231); background-color: rgb(220, 252, 231);
color: rgb(22, 163, 74); color: rgb(22, 163, 74);
} }
.statusBadgeUpdated { .statusBadgeUpdated {
background-color: rgb(219, 234, 254); background-color: rgb(219, 234, 254);
color: rgb(37, 99, 235); color: rgb(37, 99, 235);
} }
.titleCell { .titleCell {
font-weight: 500; font-weight: 500;
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.cell { .cell {
font-size: 0.875rem; font-size: 0.875rem;
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
} }
.cellTruncate { .cellTruncate {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.viewButton { .viewButton {
background-color: rgb(37, 99, 235); background-color: rgb(37, 99, 235);
color: white; color: white;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
.viewButton:hover { .viewButton:hover {
background-color: rgb(29, 78, 216); background-color: rgb(29, 78, 216);
} }
@media (max-width: 1024px) { @media (max-width: 1024px) {
.statsGrid { .statsGrid {
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.tableHeader, .tableHeader,
.tableRow { .tableRow {
grid-template-columns: 0.6fr 1.5fr 0.8fr 1fr 0.8fr 0.6fr 0.5fr; grid-template-columns: 0.6fr 1.5fr 0.8fr 1fr 0.8fr 0.6fr 0.5fr;
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.statsGrid { .statsGrid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.tableHeader, .tableHeader,
.tableRow { .tableRow {
grid-template-columns: 0.6fr 1.2fr 0.6fr 0.8fr 0.6fr 0.5fr 0.4fr; grid-template-columns: 0.6fr 1.2fr 0.6fr 0.8fr 0.6fr 0.5fr 0.4fr;
gap: 0.5rem; gap: 0.5rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
} }
.headerCell, .headerCell,
.cell { .cell {
font-size: 0.75rem; font-size: 0.75rem;
} }
} }