Mise à jour interface et styles frontend
Build & Deploy / build (push) Successful in 28s

This commit is contained in:
Mouigni
2026-08-16 17:25:45 +02:00
parent b86bd6fbc2
commit 12fc18ea9d
18 changed files with 46 additions and 91 deletions
+1 -3
View File
@@ -14,9 +14,7 @@
.main { .main {
flex: 1; flex: 1;
/* Sans min-width:0 un élément flex garde min-width:auto et déborde de son
conteneur dès qu'il contient un bloc large (tableau, titre non coupable) :
la page entière part alors plus large que l'écran. */
min-width: 0; min-width: 0;
margin-left: 16rem; margin-left: 16rem;
padding: 2rem; padding: 2rem;
+2 -4
View File
@@ -104,9 +104,7 @@ export default function App() {
: location.pathname === "/admin"? "admin" : location.pathname === "/admin"? "admin"
: "home"; : "home";
// Le user de localStorage est un instantané pris au login : un changement de
// rôle en base n'y apparaît jamais. On le resynchronise depuis /me au montage,
// sinon un compte promu admin reste bloqué hors de /admin jusqu'à reconnexion.
useEffect(() => { useEffect(() => {
const token = localStorage.getItem("authToken"); const token = localStorage.getItem("authToken");
if (!token) return; if (!token) return;
@@ -155,7 +153,7 @@ export default function App() {
); );
} }
// ← CookieBanner visible même sans connexion
if (!authState.isAuthenticated) { if (!authState.isAuthenticated) {
return ( return (
<> <>
-1
View File
@@ -10,7 +10,6 @@ type PaginationProps = {
type PageEntry = number | "ellipsis"; 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[] { function buildPageList(currentPage: number, totalPages: number): PageEntry[] {
if (totalPages <= MAX_VISIBLE_PAGES) { if (totalPages <= MAX_VISIBLE_PAGES) {
return Array.from({ length: totalPages }, (_, index) => index + 1); return Array.from({ length: totalPages }, (_, index) => index + 1);
-4
View File
@@ -10,7 +10,6 @@ export default function AboutPage() {
<p className={styles.subtitle}>Plateforme de veille réglementaire pour les ingrédients cosmétiques</p> <p className={styles.subtitle}>Plateforme de veille réglementaire pour les ingrédients cosmétiques</p>
</div> </div>
{/* Mission Section */}
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<div className={styles.sectionIcon}> <div className={styles.sectionIcon}>
@@ -26,7 +25,6 @@ export default function AboutPage() {
</p> </p>
</div> </div>
{/* Pour Qui Section */}
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<div className={styles.sectionIcon}> <div className={styles.sectionIcon}>
@@ -74,7 +72,6 @@ export default function AboutPage() {
</div> </div>
</div> </div>
{/* Key Features */}
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<div className={styles.sectionIcon}> <div className={styles.sectionIcon}>
@@ -110,7 +107,6 @@ export default function AboutPage() {
</div> </div>
</div> </div>
{/* About Platform */}
<div className={styles.section}> <div className={styles.section}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<div className={styles.sectionIcon}> <div className={styles.sectionIcon}>
+1 -4
View File
@@ -9,9 +9,7 @@ type Document = { id: string; title: string; source: 'CIR' | 'SCCS'; ingredient?
type ScraperLog = { id: string; when: string; source: string; status: 'pending' | 'running' | 'success' | 'error' } type ScraperLog = { id: string; when: string; source: string; status: 'pending' | 'running' | 'success' | 'error' }
type Stats = { total_documents?: number; cir_documents?: number; sccs_documents?: number; total_users?: number } type Stats = { total_documents?: number; cir_documents?: number; sccs_documents?: number; total_users?: number }
// Un jeton absent ou expiré (30 min de validité) renvoie 401. Le message
// générique laissait croire à une panne du serveur alors qu'il suffit de se
// reconnecter — l'interface, elle, croit toujours la session ouverte.
function assertOk(res: Response, fallback: string){ function assertOk(res: Response, fallback: string){
if(res.ok) return if(res.ok) return
if(res.status === 401){ if(res.status === 401){
@@ -42,7 +40,6 @@ export default function Admin() {
const [sortKey, setSortKey] = useState<string | null>(null) const [sortKey, setSortKey] = useState<string | null>(null)
const [sortDir, setSortDir] = useState<'asc'|'desc'>('asc') const [sortDir, setSortDir] = useState<'asc'|'desc'>('asc')
// Jeton posé par App.tsx au login (handleAuthSuccess).
const token = localStorage.getItem('authToken') ?? '' const token = localStorage.getItem('authToken') ?? ''
useEffect(() => { useEffect(() => {
-1
View File
@@ -72,7 +72,6 @@ export default function AuthPage({ onAuthSuccess }: { onAuthSuccess: (token: str
const userData = await response.json(); const userData = await response.json();
// Automatiquement connecter après l'inscription
const loginResponse = await fetch(`${API_URL}/login`, { const loginResponse = await fetch(`${API_URL}/login`, {
method: "POST", method: "POST",
headers: { headers: {
-5
View File
@@ -35,11 +35,8 @@ export default function ContactPage() {
<p className={styles.subtitle}>Nous sommes à votre écoute pour toute question ou demande d'information</p> <p className={styles.subtitle}>Nous sommes à votre écoute pour toute question ou demande d'information</p>
</div> </div>
{/* Main Content */}
<div className={styles.mainContent}> <div className={styles.mainContent}>
{/* Contact Info */}
<div className={styles.contactInfo}> <div className={styles.contactInfo}>
{/* Email */}
<div className={styles.infoSection}> <div className={styles.infoSection}>
<h3 className={styles.infoSectionTitle}> <h3 className={styles.infoSectionTitle}>
<Mail size={20} className={styles.infoIcon} /> <Mail size={20} className={styles.infoIcon} />
@@ -55,7 +52,6 @@ export default function ContactPage() {
</div> </div>
</div> </div>
{/* Phone */}
<div className={styles.infoSection}> <div className={styles.infoSection}>
<h3 className={styles.infoSectionTitle}> <h3 className={styles.infoSectionTitle}>
<Phone size={20} className={styles.infoIcon} /> <Phone size={20} className={styles.infoIcon} />
@@ -208,7 +204,6 @@ export default function ContactPage() {
</div> </div>
</div> </div>
{/* FAQ Section */}
<div className={styles.faqSection}> <div className={styles.faqSection}>
<h2 className={styles.faqTitle}>Questions Fréquentes</h2> <h2 className={styles.faqTitle}>Questions Fréquentes</h2>
<div className={styles.faqItems}> <div className={styles.faqItems}>
-5
View File
@@ -106,7 +106,6 @@ export default function DocumentsPage() {
safePage * itemsPerPage safePage * itemsPerPage
); );
// adjust items per page based on viewport width
useEffect(() => { useEffect(() => {
const apply = () => { const apply = () => {
const w = window.innerWidth; const w = window.innerWidth;
@@ -130,7 +129,6 @@ export default function DocumentsPage() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* 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}>
@@ -138,7 +136,6 @@ export default function DocumentsPage() {
</p> </p>
</div> </div>
{/* FILTERS */}
<div className={styles.filtersBox}> <div className={styles.filtersBox}>
<div className={styles.filterLabel}> <div className={styles.filterLabel}>
<Filter size={16} /> <Filter size={16} />
@@ -196,7 +193,6 @@ export default function DocumentsPage() {
</div> </div>
</div> </div>
{/* STATS */}
<div className={styles.statsRow}> <div className={styles.statsRow}>
<p className={styles.statsText}> <p className={styles.statsText}>
Total Documents: Total Documents:
@@ -207,7 +203,6 @@ export default function DocumentsPage() {
</p> </p>
</div> </div>
{/* TABLE */}
<div className={styles.table}> <div className={styles.table}>
<div className={styles.tableHeader}> <div className={styles.tableHeader}>
<span>Title</span> <span>Title</span>
-3
View File
@@ -22,7 +22,6 @@ export default function HomePage({ handleNavClick, recentDocuments }: HomePagePr
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* Hero Section */}
<div className={styles.heroSection}> <div className={styles.heroSection}>
<h1 className={styles.title}>RegWatch MedLabs</h1> <h1 className={styles.title}>RegWatch MedLabs</h1>
<p className={styles.subtitle}>Cosmetic Regulatory Monitoring Platform</p> <p className={styles.subtitle}>Cosmetic Regulatory Monitoring Platform</p>
@@ -31,7 +30,6 @@ export default function HomePage({ handleNavClick, recentDocuments }: HomePagePr
</p> </p>
</div> </div>
{/* Feature Cards */}
<div className={styles.cardsGrid}> <div className={styles.cardsGrid}>
<button onClick={() => handleNavClick('ingredient-search')} className={styles.card}> <button onClick={() => handleNavClick('ingredient-search')} className={styles.card}>
<div className={styles.cardIconWrapper}> <div className={styles.cardIconWrapper}>
@@ -58,7 +56,6 @@ export default function HomePage({ handleNavClick, recentDocuments }: HomePagePr
</button> </button>
</div> </div>
{/* Recent Documents */}
<div className={styles.recentSection}> <div className={styles.recentSection}>
<div className={styles.sectionHeader}> <div className={styles.sectionHeader}>
<Clock size={20} className={styles.sectionIcon} /> <Clock size={20} className={styles.sectionIcon} />
-9
View File
@@ -15,7 +15,6 @@ export default function IngredientSearchPage() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(8); const [itemsPerPage, setItemsPerPage] = useState(8);
// FETCH API
useEffect(() => { useEffect(() => {
getDocuments() getDocuments()
.then((data) => setDocuments(Array.isArray(data) ? data : [])) .then((data) => setDocuments(Array.isArray(data) ? data : []))
@@ -26,7 +25,6 @@ export default function IngredientSearchPage() {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
// responsive items per page
useEffect(() => { useEffect(() => {
const apply = () => { const apply = () => {
const w = window.innerWidth; const w = window.innerWidth;
@@ -39,7 +37,6 @@ export default function IngredientSearchPage() {
return () => window.removeEventListener('resize', apply); return () => window.removeEventListener('resize', apply);
}, []); }, []);
// 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)),
@@ -55,7 +52,6 @@ export default function IngredientSearchPage() {
...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
const filtered = useMemo(() => { const filtered = useMemo(() => {
return documents return documents
.filter((d) => { .filter((d) => {
@@ -82,13 +78,11 @@ export default function IngredientSearchPage() {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* 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 */}
<div className={styles.searchBarWrapper}> <div className={styles.searchBarWrapper}>
<div className={styles.searchBar}> <div className={styles.searchBar}>
<input <input
@@ -102,7 +96,6 @@ export default function IngredientSearchPage() {
</div> </div>
</div> </div>
{/* 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} />
@@ -121,12 +114,10 @@ export default function IngredientSearchPage() {
</div> </div>
</div> </div>
{/* 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 */}
<div className={styles.documentsList}> <div className={styles.documentsList}>
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className={styles.emptyState}> <div className={styles.emptyState}>
-5
View File
@@ -81,7 +81,6 @@ export default function RecentUpdatesPage() {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.maxWidth}> <div className={styles.maxWidth}>
{/* 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} />
@@ -92,7 +91,6 @@ export default function RecentUpdatesPage() {
</p> </p>
</div> </div>
{/* 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>
@@ -111,7 +109,6 @@ export default function RecentUpdatesPage() {
</div> </div>
</div> </div>
{/* FILTERS */}
<div className={styles.filtersBox}> <div className={styles.filtersBox}>
<div className={styles.filterLabel}> <div className={styles.filterLabel}>
<Filter size={16} /> <Filter size={16} />
@@ -143,13 +140,11 @@ export default function RecentUpdatesPage() {
</select> </select>
</div> </div>
{/* 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 */}
<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>
-2
View File
@@ -1,5 +1,3 @@
// Bakée au build par Vite (ARG/ENV VITE_API_URL du Dockerfile).
// Repli sur l'API locale pour le dev : `pnpm dev` sans .env continue de marcher.
export const API_URL = import.meta.env.VITE_API_URL ?? "http://127.0.0.1:8000"; export const API_URL = import.meta.env.VITE_API_URL ?? "http://127.0.0.1:8000";
export type ApiDocument = { export type ApiDocument = {
+3 -34
View File
@@ -1,12 +1,4 @@
/* Administration — aligné sur la DA des autres pages :
accent violet #7c3aed (celui de la Sidebar), échelle de gris Tailwind,
cartes blanches bordées, rayons 0.5/0.75rem, unités rem. */
/* main.tsx ne charge pas styles/global.css — vestige d'une ancienne maquette,
jamais importé — donc son `* { box-sizing: border-box }` ne s'applique pas et
l'application tourne en content-box : tout width:100% assorti d'un padding
déborde de son conteneur. Réinitialisation cantonnée à cette page pour ne rien
déplacer sur le reste du site. */
.page, .page,
.page *, .page *,
.page *::before, .page *::before,
@@ -22,7 +14,6 @@
margin: 0 auto; margin: 0 auto;
} }
/* ── En-tête et onglets ─────────────────────────────────────── */
.header { .header {
display: flex; display: flex;
@@ -47,8 +38,6 @@
gap: 0.5rem; gap: 0.5rem;
min-width: 0; min-width: 0;
max-width: 100%; max-width: 100%;
/* Les onglets passent à la ligne quand ils ne tiennent plus, plutôt que de
défiler : aucun n'est masqué hors champ. */
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -88,7 +77,6 @@
margin-top: 0.5rem; margin-top: 0.5rem;
} }
/* ── États ──────────────────────────────────────────────────── */
.spinner { .spinner {
padding: 0.875rem 1rem; padding: 0.875rem 1rem;
@@ -114,7 +102,6 @@
color: rgb(107, 114, 128); color: rgb(107, 114, 128);
} }
/* ── Barre de panneau ───────────────────────────────────────── */
.panelHeader { .panelHeader {
display: flex; display: flex;
@@ -137,8 +124,6 @@
max-width: 22rem; max-width: 22rem;
} }
/* ── Tableaux ───────────────────────────────────────────────── */
.tableWrap { .tableWrap {
background: white; background: white;
border: 1px solid rgb(229, 231, 235); border: 1px solid rgb(229, 231, 235);
@@ -161,7 +146,6 @@
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
/* Sans cette borne, un seul titre long étire la colonne et donc tout le tableau. */
.table td:nth-child(2) { .table td:nth-child(2) {
min-width: 12rem; min-width: 12rem;
max-width: 26rem; max-width: 26rem;
@@ -223,7 +207,6 @@
color: white; color: white;
} }
/* ── Badges ─────────────────────────────────────────────────── */
.badgeAdmin, .badgeAdmin,
.badgeUser, .badgeUser,
@@ -270,7 +253,6 @@
border: 1px solid rgba(16, 185, 129, 0.3); border: 1px solid rgba(16, 185, 129, 0.3);
} }
/* ── Statistiques ───────────────────────────────────────────── */
.cards { .cards {
display: grid; display: grid;
@@ -342,8 +324,7 @@
color: rgb(55, 65, 81); color: rgb(55, 65, 81);
} }
/* La largeur est posée en style inline par le composant : min-width garde une
amorce visible quand la valeur est nulle. */
.barFill { .barFill {
height: 0.625rem; height: 0.625rem;
min-width: 0.625rem; min-width: 0.625rem;
@@ -363,7 +344,6 @@
color: rgb(75, 85, 99); color: rgb(75, 85, 99);
} }
/* ── Scrapers ───────────────────────────────────────────────── */
.scraperControls { .scraperControls {
display: grid; display: grid;
@@ -432,7 +412,6 @@
color: rgb(17, 24, 39); color: rgb(17, 24, 39);
} }
/* ── Modale ─────────────────────────────────────────────────── */
.modalBackdrop { .modalBackdrop {
position: fixed; position: fixed;
@@ -478,7 +457,6 @@
color: white; color: white;
} }
/* ── Responsive ─────────────────────────────────────────────── */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.scraperControls { .scraperControls {
@@ -509,8 +487,6 @@
.tabs { .tabs {
width: 100%; width: 100%;
} }
/* Chaque ligne devient une carte, avec l'intitulé de colonne en regard. */
.tableWrap { .tableWrap {
background: transparent; background: transparent;
border: none; border: none;
@@ -548,10 +524,6 @@
background: white; background: white;
} }
/* Surtout pas display:flex ici : le texte de la cellule deviendrait un élément
flex anonyme, avec min-width:auto — un titre long refuserait de se réduire et
sortirait de la carte, en entraînant toute la page. En bloc, l'intitulé passe
au-dessus de la valeur et la coupure de mots suffit. */
.table td { .table td {
display: block; display: block;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
@@ -572,8 +544,7 @@
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
} }
/* `.table td { display: block }` l'emporte en spécificité sur `.rowActions` :
on le rétablit ici pour garder les actions sur une rangée. */
.table td.rowActions { .table td.rowActions {
display: flex; display: flex;
} }
@@ -614,6 +585,4 @@
} }
} }
/* NB : le site n'a pas de mode sombre réel — index.css bascule --bg sur le body
mais aucune page n'utilise ces variables et toutes gardent des cartes claires.
Cette page suit la même convention pour rester cohérente avec ses sœurs. */
+12 -1
View File
@@ -1 +1,12 @@
.btn{position:fixed;right:20px;bottom:20px;background:#1e293b;color:#fff;border:none;border-radius:999px;width:44px;height:44px;font-size:20px;cursor:pointer;box-shadow:0 8px 20px rgba(2,6,23,0.2);z-index:12000} .btn{
position:fixed;
right:20px;bottom:20px;
background:#1e293b;
color:#fff;border:none;
border-radius:999px;
width:44px;
height:44px;
font-size:20px;
cursor:pointer;
box-shadow:0 8px 20px rgba(2,6,23,0.2);
z-index:12000}
-3
View File
@@ -1,6 +1,3 @@
/* Style unique de pagination, partagé par toutes les pages.
Reprend l'accent bleu #2563eb du site (boutons primaires, liens d'action). */
.pagination { .pagination {
display: flex; display: flex;
align-items: center; align-items: center;
-1
View File
@@ -194,5 +194,4 @@
.open{transform:translateX(0)} .open{transform:translateX(0)}
/* overlay will be handled by a simple full-screen element appended in DOM via JS if needed */
} }
+27 -5
View File
@@ -1,5 +1,27 @@
.container{position:fixed;left:16px;bottom:24px;display:flex;flex-direction:column-reverse;gap:8px;z-index:12000} .container{
.toast{padding:10px 14px;border-radius:8px;color:#fff;box-shadow:0 6px 18px rgba(2,6,23,0.12);min-width:200px} position:fixed;
.info{background:#334155} left:16px;
.success{background:#059669} bottom:24px;
.error{background:#ef4444} display:flex;
flex-direction:column-reverse;
gap:8px;
z-index:12000
}
.toast{padding:10px 14px;
border-radius:8px
;color:#fff;
box-shadow:0 6px 18px rgba(2,6,23,0.12);
min-width:200px
}
.info{
background:#334155
}
.success{
background:#059669
}
.error{
background:#ef4444
}
-1
View File
@@ -4,7 +4,6 @@ body {
background: #f4f6f8; background: #f4f6f8;
} }
/* Global required rules */
* { box-sizing: border-box; } * { box-sizing: border-box; }
img, table { max-width: 100%; } img, table { max-width: 100%; }