This commit is contained in:
+1
-3
@@ -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
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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(() => {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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. */
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 */
|
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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%; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user