fix(frontend): finitions DSFR (ThemeToggle orphelin, confirm() -> ConfirmDialog, tooltips)

- supprime ThemeToggle orphelin (inerte, forçait le thème sombre)
- remplace les confirm() natifs par ConfirmDialog DSFR sur les 3 pages admin
  (utilisateurs, contenus, émotions)
- tooltips Recharts en variables DSFR (lisibles en clair ET sombre)

tsc 0, build OK.
This commit is contained in:
Azmog
2026-06-19 10:57:01 +02:00
parent fe9e921d73
commit f18fb5450b
5 changed files with 73 additions and 74 deletions
@@ -6,6 +6,7 @@ import type { Feed } from '@/types';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export default function AdminContenusPage() { export default function AdminContenusPage() {
@@ -14,6 +15,7 @@ export default function AdminContenusPage() {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingFeed, setEditingFeed] = useState<Feed | null>(null); const [editingFeed, setEditingFeed] = useState<Feed | null>(null);
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [form, setForm] = useState({ const [form, setForm] = useState({
titre: '', titre: '',
contenu: '', contenu: '',
@@ -74,14 +76,16 @@ export default function AdminContenusPage() {
} }
}; };
const handleDelete = async (id: number) => { const confirmDelete = async () => {
if (!confirm('Supprimer cet article ?')) return; if (deleteId === null) return;
try { try {
await api.delete(`/admin/feeds/${id}`); await api.delete(`/admin/feeds/${deleteId}`);
await fetchFeeds(); await fetchFeeds();
toast.success('Article supprimé'); toast.success('Article supprimé');
} catch { } catch {
toast.error('Erreur lors de la suppression'); toast.error('Erreur lors de la suppression');
} finally {
setDeleteId(null);
} }
}; };
@@ -152,7 +156,7 @@ export default function AdminContenusPage() {
<li> <li>
<button <button
type="button" type="button"
onClick={() => handleDelete(feed.id)} onClick={() => setDeleteId(feed.id)}
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line" className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
title="Supprimer" title="Supprimer"
> >
@@ -238,6 +242,16 @@ export default function AdminContenusPage() {
</ul> </ul>
</form> </form>
</Modal> </Modal>
<ConfirmDialog
isOpen={deleteId !== null}
title="Supprimer l'article"
message="Cet article sera supprimé définitivement. Cette action est irréversible."
confirmLabel="Supprimer"
destructive
onConfirm={confirmDelete}
onClose={() => setDeleteId(null)}
/>
</div> </div>
); );
} }
@@ -6,6 +6,7 @@ import type { Emotion } from '@/types';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export default function AdminEmotionsPage() { export default function AdminEmotionsPage() {
@@ -14,6 +15,7 @@ export default function AdminEmotionsPage() {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [editingEmotion, setEditingEmotion] = useState<Emotion | null>(null); const [editingEmotion, setEditingEmotion] = useState<Emotion | null>(null);
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [form, setForm] = useState({ const [form, setForm] = useState({
nom: '', nom: '',
couleur: '#fce117', couleur: '#fce117',
@@ -86,14 +88,18 @@ export default function AdminEmotionsPage() {
} }
}; };
const handleDelete = async (id: number) => { const handleDelete = (id: number) => setDeleteId(id);
if (!confirm('Supprimer cette émotion ?')) return;
const confirmDelete = async () => {
if (deleteId === null) return;
try { try {
await api.delete(`/admin/emotions/${id}`); await api.delete(`/admin/emotions/${deleteId}`);
await fetchEmotions(); await fetchEmotions();
toast.success('Émotion supprimée'); toast.success('Émotion supprimée');
} catch { } catch {
toast.error('Erreur lors de la suppression'); toast.error('Erreur lors de la suppression');
} finally {
setDeleteId(null);
} }
}; };
@@ -253,6 +259,16 @@ export default function AdminEmotionsPage() {
</ul> </ul>
</form> </form>
</Modal> </Modal>
<ConfirmDialog
isOpen={deleteId !== null}
title="Supprimer l'émotion"
message="Cette émotion (et ses sous-émotions) sera supprimée. Cette action est irréversible."
confirmLabel="Supprimer"
destructive
onConfirm={confirmDelete}
onClose={() => setDeleteId(null)}
/>
</div> </div>
); );
} }
@@ -6,6 +6,7 @@ import type { Utilisateur } from '@/types';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import ConfirmDialog from '@/components/ui/ConfirmDialog';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export default function AdminUtilisateursPage() { export default function AdminUtilisateursPage() {
@@ -14,6 +15,7 @@ export default function AdminUtilisateursPage() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [createModalOpen, setCreateModalOpen] = useState(false); const [createModalOpen, setCreateModalOpen] = useState(false);
const [formLoading, setFormLoading] = useState(false); const [formLoading, setFormLoading] = useState(false);
const [deleteId, setDeleteId] = useState<number | null>(null);
const [newUser, setNewUser] = useState({ const [newUser, setNewUser] = useState({
nom: '', prenom: '', email: '', password: '', password_confirmation: '', role_id: 2, nom: '', prenom: '', email: '', password: '', password_confirmation: '', role_id: 2,
}); });
@@ -41,14 +43,16 @@ export default function AdminUtilisateursPage() {
} }
}; };
const handleDelete = async (id: number) => { const confirmDelete = async () => {
if (!confirm('Supprimer cet utilisateur ?')) return; if (deleteId === null) return;
try { try {
await api.delete(`/admin/utilisateurs/${id}`); await api.delete(`/admin/utilisateurs/${deleteId}`);
await fetchUsers(); await fetchUsers();
toast.success('Utilisateur supprimé'); toast.success('Utilisateur supprimé');
} catch { } catch {
toast.error('Erreur lors de la suppression'); toast.error('Erreur lors de la suppression');
} finally {
setDeleteId(null);
} }
}; };
@@ -170,7 +174,7 @@ export default function AdminUtilisateursPage() {
<li> <li>
<button <button
type="button" type="button"
onClick={() => handleDelete(user.id)} onClick={() => setDeleteId(user.id)}
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line" className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
title="Supprimer" title="Supprimer"
> >
@@ -222,6 +226,16 @@ export default function AdminUtilisateursPage() {
</ul> </ul>
</form> </form>
</Modal> </Modal>
<ConfirmDialog
isOpen={deleteId !== null}
title="Supprimer l'utilisateur"
message="Cet utilisateur sera supprimé. Cette action est irréversible."
confirmLabel="Supprimer"
destructive
onConfirm={confirmDelete}
onClose={() => setDeleteId(null)}
/>
</div> </div>
); );
} }
@@ -60,11 +60,13 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
[`${value} saisies`, name] [`${value} saisies`, name]
} }
contentStyle={{ contentStyle={{
backgroundColor: 'rgba(255,255,255,0.95)', backgroundColor: 'var(--background-overlap-grey)',
border: 'none', border: '1px solid var(--border-default-grey)',
borderRadius: '12px', borderRadius: '0.25rem',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', color: 'var(--text-default-grey)',
}} }}
labelStyle={{ color: 'var(--text-default-grey)' }}
itemStyle={{ color: 'var(--text-default-grey)' }}
/> />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -94,11 +96,13 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
<YAxis tick={{ fontSize: 12 }} allowDecimals={false} /> <YAxis tick={{ fontSize: 12 }} allowDecimals={false} />
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
backgroundColor: 'rgba(255,255,255,0.95)', backgroundColor: 'var(--background-overlap-grey)',
border: 'none', border: '1px solid var(--border-default-grey)',
borderRadius: '12px', borderRadius: '0.25rem',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', color: 'var(--text-default-grey)',
}} }}
labelStyle={{ color: 'var(--text-default-grey)' }}
itemStyle={{ color: 'var(--text-default-grey)' }}
/> />
<Legend /> <Legend />
{data.evolution.emotions.map((emotion) => ( {data.evolution.emotions.map((emotion) => (
@@ -134,11 +138,13 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
<YAxis domain={[0, 10]} tick={{ fontSize: 12 }} /> <YAxis domain={[0, 10]} tick={{ fontSize: 12 }} />
<Tooltip <Tooltip
contentStyle={{ contentStyle={{
backgroundColor: 'rgba(255,255,255,0.95)', backgroundColor: 'var(--background-overlap-grey)',
border: 'none', border: '1px solid var(--border-default-grey)',
borderRadius: '12px', borderRadius: '0.25rem',
boxShadow: '0 4px 12px rgba(0,0,0,0.1)', color: 'var(--text-default-grey)',
}} }}
labelStyle={{ color: 'var(--text-default-grey)' }}
itemStyle={{ color: 'var(--text-default-grey)' }}
formatter={(value: ValueType | undefined) => [`${Number(value).toFixed(1)}`, 'Intensité moyenne']} formatter={(value: ValueType | undefined) => [`${Number(value).toFixed(1)}`, 'Intensité moyenne']}
/> />
<Bar dataKey="intensite" fill="#000091" radius={[6, 6, 0, 0]} /> <Bar dataKey="intensite" fill="#000091" radius={[6, 6, 0, 0]} />
@@ -1,51 +0,0 @@
"use client";
import React from "react";
type Scheme = "light" | "dark";
export default function ThemeToggle() {
const [mounted, setMounted] = React.useState(false);
const [scheme, setScheme] = React.useState<Scheme>("light");
React.useEffect(() => {
setMounted(true);
const current = document.documentElement.getAttribute("data-fr-theme");
setScheme(current === "dark" ? "dark" : "light");
}, []);
const toggle = () => {
const next: Scheme = scheme === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-fr-theme", next);
document.documentElement.setAttribute("data-fr-scheme", next);
setScheme(next);
};
if (!mounted) {
return (
<button
type="button"
className="fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-theme-fill"
aria-label="Changer le thème"
>
Thème
</button>
);
}
return (
<button
type="button"
onClick={toggle}
className={`fr-btn fr-btn--tertiary fr-btn--icon-left ${
scheme === "dark" ? "fr-icon-sun-fill" : "fr-icon-moon-fill"
}`}
aria-label={
scheme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"
}
title={scheme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
>
{scheme === "dark" ? "Mode clair" : "Mode sombre"}
</button>
);
}