Zlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i
zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7
zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG
z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S
zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr
z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S
zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er
zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa
zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc-
zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V
zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I
zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc
z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E(
zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef
LrJugUA?W`A8`#=m
literal 0
HcmV?d00001
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
new file mode 100644
index 0000000..6047ae7
--- /dev/null
+++ b/frontend/src/app/globals.css
@@ -0,0 +1,80 @@
+@import "tailwindcss";
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: "Inter", system-ui, sans-serif;
+ --font-display: "Poppins", system-ui, sans-serif;
+
+ /* Candlelight */
+ --color-candlelight-50: #fffde6;
+ --color-candlelight-100: #fffacc;
+ --color-candlelight-200: #fff599;
+ --color-candlelight-300: #ffef66;
+ --color-candlelight-400: #ffe833;
+ --color-candlelight-500: #fce117;
+ --color-candlelight-600: #e5c800;
+ --color-candlelight-700: #b39c00;
+ --color-candlelight-800: #806f00;
+ --color-candlelight-900: #4d4300;
+ --color-candlelight-950: #332d00;
+
+ /* Malachite */
+ --color-malachite-50: #e6fdf0;
+ --color-malachite-100: #ccfbe0;
+ --color-malachite-200: #99f7c2;
+ --color-malachite-300: #66f3a3;
+ --color-malachite-400: #33ef85;
+ --color-malachite-500: #06c656;
+ --color-malachite-600: #05a348;
+ --color-malachite-700: #047d37;
+ --color-malachite-800: #035827;
+ --color-malachite-900: #023316;
+ --color-malachite-950: #01220f;
+}
+
+/* Variables de thème clair */
+:root {
+ --background: #ffffff;
+ --foreground: #171717;
+ --primary: #fce117;
+ --primary-foreground: #332d00;
+ --secondary: #06c656;
+ --secondary-foreground: #01220f;
+ --muted: #f5f5f5;
+ --muted-foreground: #737373;
+ --border: #e5e5e5;
+ --ring: #fce117;
+}
+
+/* Variables de thème sombre */
+.dark {
+ --background: #0a0a0a;
+ --foreground: #ededed;
+ --primary: #fce117;
+ --primary-foreground: #332d00;
+ --secondary: #06c656;
+ --secondary-foreground: #01220f;
+ --muted: #262626;
+ --muted-foreground: #a3a3a3;
+ --border: #404040;
+ --ring: #fce117;
+}
+
+/* Styles de base */
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: "Inter", system-ui, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: "Poppins", system-ui, sans-serif;
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
new file mode 100644
index 0000000..67b17c4
--- /dev/null
+++ b/frontend/src/app/layout.tsx
@@ -0,0 +1,38 @@
+import type { Metadata } from "next";
+import { Inter, Poppins } from "next/font/google";
+import "./globals.css";
+import Providers from "@/components/Providers";
+
+const inter = Inter({
+ variable: "--font-inter",
+ subsets: ["latin"],
+ weight: ["400", "500", "600", "700"],
+ display: "swap",
+});
+
+const poppins = Poppins({
+ variable: "--font-poppins",
+ subsets: ["latin"],
+ weight: ["500", "600", "700"],
+ display: "swap",
+});
+
+export const metadata: Metadata = {
+ title: "CESIZen - Votre sant\u00e9 mentale",
+ description:
+ "Plateforme de gestion du stress et suivi \u00e9motionnel. Suivez vos \u00e9motions, consultez des ressources et prenez soin de votre sant\u00e9 mentale.",
+};
+
+export default function RootLayout({
+ children,
+}: Readonly<{
+ children: React.ReactNode;
+}>) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
new file mode 100644
index 0000000..f9fce5a
--- /dev/null
+++ b/frontend/src/app/page.tsx
@@ -0,0 +1,592 @@
+'use client';
+
+import { useEffect, useRef } from 'react';
+import Link from 'next/link';
+import {
+ BookOpen,
+ Heart,
+ BarChart3,
+ Shield,
+ ArrowRight,
+ Sparkles,
+ Brain,
+ TrendingUp,
+ Users,
+ CheckCircle2,
+ Star,
+ Zap,
+ Clock,
+ SmilePlus,
+} from 'lucide-react';
+
+// Hook d'animation au scroll (Intersection Observer)
+function useScrollReveal() {
+ const ref = useRef(null);
+ useEffect(() => {
+ const el = ref.current;
+ if (!el) return;
+ const observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ el.classList.add('animate-in');
+ observer.unobserve(el);
+ }
+ },
+ { threshold: 0.15 }
+ );
+ observer.observe(el);
+ return () => observer.disconnect();
+ }, []);
+ return ref;
+}
+
+function RevealSection({ children, className = '', delay = 0 }: { children: React.ReactNode; className?: string; delay?: number }) {
+ const ref = useScrollReveal();
+ return (
+
+ {children}
+
+ );
+}
+
+export default function HomePage() {
+ return (
+
+
+
+ {/* ─── Hero Section ─── */}
+
+ {/* Orbes décoratives */}
+
+
+ {/* Grille de fond */}
+
+
+
+
+
+
+
+
+ Plateforme du Ministère des Solidarités et de la Santé
+
+
+
+
+
+
+ CESI
+ ZEN
+
+
+
+
+
+ Votre compagnon pour une{' '}
+ santé mentale {' '}
+ épanouie
+
+
+
+
+
+ Suivez vos émotions, comprenez vos tendances et prenez soin de vous
+ au quotidien avec des outils simples, bienveillants et confidentiels.
+
+
+
+
+
+
+ Commencer gratuitement
+
+
+
+
+ En savoir plus
+
+
+
+
+ {/* Mini stats */}
+
+
+ } value="100%" label="Gratuit" />
+ } value="RGPD" label="Conforme" />
+ } value="2 min" label="Pour commencer" />
+
+
+
+
+
+ {/* Séparateur vague */}
+
+
+
+ {/* ─── Comment ça marche ─── */}
+
+
+
+
+
+
+ Simple et rapide
+
+
+ Comment ça marche ?
+
+
+ Trois étapes simples pour prendre soin de votre bien-être émotionnel.
+
+
+
+
+
+ {/* Ligne de connexion */}
+
+
+
+ }
+ title="Identifiez votre émotion"
+ description="Choisissez parmi un panel d'émotions et sous-émotions. Notez l'intensité ressentie sur une échelle de 1 à 10."
+ color="candlelight"
+ />
+
+
+ }
+ title="Ajoutez du contexte"
+ description="Notez ce qui a déclenché cette émotion. Même quelques mots suffisent à mieux comprendre vos tendances."
+ color="malachite"
+ />
+
+
+ }
+ title="Suivez votre évolution"
+ description="Consultez vos rapports visuels pour identifier vos tendances émotionnelles sur la semaine, le mois ou l'année."
+ color="candlelight"
+ />
+
+
+
+
+
+ {/* ─── Features Section ─── */}
+
+
+
+
+
+
+
+
+
+ Fonctionnalités
+
+
+ Tout ce dont vous avez besoin
+
+
+ Des outils conçus pour vous accompagner au quotidien dans la gestion
+ de votre bien-être émotionnel.
+
+
+
+
+
+ {[
+ {
+ icon: ,
+ title: "Journal d'émotions",
+ description: "Enregistrez vos émotions quotidiennes et suivez leur évolution dans le temps grâce à un journal intime sécurisé.",
+ color: 'malachite' as const,
+ delay: 0,
+ },
+ {
+ icon: ,
+ title: 'Rapports visuels',
+ description: "Visualisez vos tendances grâce à des graphiques clairs : répartition, évolution et intensité de vos émotions.",
+ color: 'candlelight' as const,
+ delay: 100,
+ },
+ {
+ icon: ,
+ title: 'Ressources fiables',
+ description: "Accédez à des contenus validés par des experts sur la santé mentale, la gestion du stress et le bien-être.",
+ color: 'malachite' as const,
+ delay: 200,
+ },
+ {
+ icon: ,
+ title: 'Données protégées',
+ description: "Vos données sont chiffrées et protégées conformément au RGPD. Vous gardez le contrôle total sur vos informations.",
+ color: 'candlelight' as const,
+ delay: 300,
+ },
+ ].map((feature) => (
+
+
+
+ ))}
+
+
+
+
+ {/* ─── Témoignages / Social Proof ─── */}
+
+
+
+
+
+
+ Pourquoi CESIZen ?
+
+
+ Conçu pour votre bien-être
+
+
+
+
+
+ {[
+ {
+ quote: "Prendre conscience de ses émotions est le premier pas vers une meilleure santé mentale.",
+ author: "Approche scientifique",
+ detail: "Basé sur les travaux de la psychologie positive",
+ delay: 0,
+ },
+ {
+ quote: "Le simple fait de nommer une émotion réduit son intensité de 50%. C'est scientifiquement prouvé.",
+ author: "Affect Labeling",
+ detail: "Étude UCLA, Lieberman et al.",
+ delay: 150,
+ },
+ {
+ quote: "L'écriture expressive améliore la santé physique et mentale sur le long terme.",
+ author: "Journaling thérapeutique",
+ detail: "Méthode Pennebaker",
+ delay: 300,
+ },
+ ].map((item) => (
+
+
+
+ {[1, 2, 3, 4, 5].map((s) => (
+
+ ))}
+
+
+ “{item.quote}”
+
+
+
{item.author}
+
{item.detail}
+
+
+
+ ))}
+
+
+
+
+ {/* ─── Checklist Section ─── */}
+
+
+
+
+
+
+
+ Avantages
+
+
+ Une application pensée pour vous
+
+
+ CESIZen est conçu par le Ministère des Solidarités et de la Santé
+ pour offrir un outil fiable, accessible et respectueux de votre vie privée.
+
+
+ {[
+ 'Totalement gratuit et sans publicité',
+ 'Conforme RGPD — vos données vous appartiennent',
+ 'Interface accessible et inclusive (RGAA)',
+ 'Mode sombre pour un usage confortable',
+ 'Rapports visuels pour comprendre vos tendances',
+ 'Contenus validés par des professionnels de santé',
+ 'Export de vos données à tout moment',
+ 'Suppression de compte en un clic',
+ ].map((item, i) => (
+
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+ {/* Carte décorative simulant l'app */}
+
+
+
+
+
+
+
Comment vous sentez-vous ?
+
Aujourd'hui à 14:30
+
+
+
+ {[
+ { emoji: '😊', label: 'Joie', color: 'bg-yellow-100 dark:bg-yellow-900/30' },
+ { emoji: '😌', label: 'Calme', color: 'bg-blue-100 dark:bg-blue-900/30' },
+ { emoji: '😤', label: 'Colère', color: 'bg-red-100 dark:bg-red-900/30' },
+ { emoji: '😢', label: 'Triste', color: 'bg-purple-100 dark:bg-purple-900/30' },
+ ].map((e) => (
+
+ {e.emoji}
+ {e.label}
+
+ ))}
+
+
+
+ Intensité
+ 7/10
+
+
+
+
+
+ “Belle journée au travail, bonne ambiance avec l'équipe...”
+
+
+
+
+ {/* Décoration */}
+
+
+
+
+
+
+
+ {/* ─── CTA Section ─── */}
+
+
+
+
+
+
+
+
+
+
+ Rejoignez la communauté
+
+
+
+
+
+ Prêt à prendre soin de vous ?
+
+
+
+
+
+ Créez votre compte en 2 minutes et commencez à suivre votre bien-être
+ émotionnel. C'est gratuit, confidentiel et sans engagement.
+
+
+
+
+
+
+ Créer mon compte
+
+
+
+ Se connecter
+
+
+
+
+
+
+ {/* ─── Urgence Banner ─── */}
+
+
+
+ En cas d'urgence , appelez le{' '}
+
+ 3114
+ {' '}
+ (numéro national de prévention du suicide) ou le{' '}
+
+ 15
+ {' '}
+ (SAMU)
+
+
+
+
+ );
+}
+
+/* ─── Composants ─── */
+
+function MiniStat({ icon, value, label }: { icon: React.ReactNode; value: string; label: string }) {
+ return (
+
+ );
+}
+
+function StepCard({
+ step,
+ icon,
+ title,
+ description,
+ color,
+}: {
+ step: number;
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+ color: 'malachite' | 'candlelight';
+}) {
+ const colors = color === 'malachite'
+ ? { bg: 'bg-malachite-50 dark:bg-malachite-950/30', icon: 'text-malachite-600 dark:text-malachite-400', iconBg: 'bg-malachite-100 dark:bg-malachite-900/50', badge: 'bg-malachite-500' }
+ : { bg: 'bg-candlelight-50 dark:bg-candlelight-950/30', icon: 'text-candlelight-600 dark:text-candlelight-400', iconBg: 'bg-candlelight-100 dark:bg-candlelight-900/50', badge: 'bg-candlelight-500' };
+
+ return (
+
+
+ {step}
+
+
+ {icon}
+
+
{title}
+
{description}
+
+ );
+}
+
+function FeatureCard({
+ icon,
+ title,
+ description,
+ color,
+}: {
+ icon: React.ReactNode;
+ title: string;
+ description: string;
+ color: 'malachite' | 'candlelight';
+}) {
+ const bgColor = color === 'malachite'
+ ? 'bg-white dark:bg-gray-800/50'
+ : 'bg-white dark:bg-gray-800/50';
+ const iconColor = color === 'malachite'
+ ? 'text-malachite-600 dark:text-malachite-400'
+ : 'text-candlelight-600 dark:text-candlelight-400';
+ const iconBg = color === 'malachite'
+ ? 'bg-malachite-100 dark:bg-malachite-900/50'
+ : 'bg-candlelight-100 dark:bg-candlelight-900/50';
+
+ return (
+
+
+ {icon}
+
+
+ {title}
+
+
+ {description}
+
+
+ );
+}
diff --git a/frontend/src/components/Providers.tsx b/frontend/src/components/Providers.tsx
new file mode 100644
index 0000000..227add5
--- /dev/null
+++ b/frontend/src/components/Providers.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { ThemeProvider } from "next-themes";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "react-hot-toast";
+import { useState } from "react";
+
+export default function Providers({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ },
+ },
+ })
+ );
+
+ return (
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/frontend/src/components/features/EmotionChart.tsx b/frontend/src/components/features/EmotionChart.tsx
new file mode 100644
index 0000000..c988407
--- /dev/null
+++ b/frontend/src/components/features/EmotionChart.tsx
@@ -0,0 +1,160 @@
+'use client';
+
+import React from 'react';
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ ResponsiveContainer,
+ PieChart,
+ Pie,
+ Cell,
+ LineChart,
+ Line,
+ Legend,
+} from 'recharts';
+import type { RapportData } from '@/types';
+
+interface EmotionChartProps {
+ data: RapportData;
+ type?: 'repartition' | 'evolution' | 'intensite';
+}
+
+export default function EmotionChart({ data, type = 'repartition' }: EmotionChartProps) {
+ if (type === 'repartition') {
+ const pieData = data.repartition.map((item) => ({
+ name: item.nom,
+ value: item.count,
+ color: item.couleur,
+ pourcentage: item.pourcentage,
+ }));
+
+ if (pieData.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+ `${name} (${pourcentage}%)`}
+ >
+ {pieData.map((entry, index) => (
+ |
+ ))}
+
+ [`${value} saisies`, name]}
+ contentStyle={{
+ backgroundColor: 'rgba(255,255,255,0.95)',
+ border: 'none',
+ borderRadius: '12px',
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
+ }}
+ />
+
+
+
+ );
+ }
+
+ if (type === 'evolution') {
+ const lineData = data.evolution.labels.map((label, index) => {
+ const point: Record = { date: label };
+ data.evolution.emotions.forEach((emotion) => {
+ point[emotion.nom] = emotion.data[index] || 0;
+ });
+ return point;
+ });
+
+ if (lineData.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {data.evolution.emotions.map((emotion) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ if (type === 'intensite') {
+ const barData = data.evolution.labels.map((label, index) => ({
+ date: label,
+ intensite: data.evolution.intensite_moyenne[index] || 0,
+ }));
+
+ if (barData.length === 0) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ [`${Number(value).toFixed(1)}`, 'Intensité moyenne']}
+ />
+
+
+
+
+ );
+ }
+
+ return null;
+}
+
+function EmptyChart({ message }: { message: string }) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/features/EmotionPicker.tsx b/frontend/src/components/features/EmotionPicker.tsx
new file mode 100644
index 0000000..2fe299c
--- /dev/null
+++ b/frontend/src/components/features/EmotionPicker.tsx
@@ -0,0 +1,105 @@
+'use client';
+
+import React, { useState } from 'react';
+import { useEmotions } from '@/hooks/useEmotions';
+import type { Emotion } from '@/types';
+
+interface EmotionPickerProps {
+ value?: number;
+ onChange: (emotionId: number) => void;
+}
+
+export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
+ const { data: emotions, isLoading } = useEmotions();
+ const [selectedParent, setSelectedParent] = useState(null);
+
+ if (isLoading) {
+ return (
+
+ {Array.from({ length: 7 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ // Si une émotion parent est sélectionnée et qu'elle a des enfants, afficher les sous-émotions
+ if (selectedParent?.enfants && selectedParent.enfants.length > 0) {
+ return (
+
+
setSelectedParent(null)}
+ className="mb-4 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 flex items-center gap-1"
+ >
+ ← Retour aux émotions
+
+
+ Précisez votre émotion de {selectedParent.nom} :
+
+
+ {selectedParent.enfants.map((child) => (
+ onChange(child.id)}
+ className={`
+ flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200
+ ${value === child.id
+ ? 'border-current shadow-lg scale-105'
+ : 'border-gray-200 dark:border-gray-700 hover:border-current hover:shadow-md'
+ }
+ `}
+ style={{
+ color: child.couleur,
+ borderColor: value === child.id ? child.couleur : undefined,
+ }}
+ >
+ {child.icone || '🔵'}
+ {child.nom}
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+ {emotions?.map((emotion) => {
+ const hasChildren = emotion.enfants && emotion.enfants.length > 0;
+ const isSelected = value === emotion.id || emotion.enfants?.some(e => e.id === value);
+
+ return (
+ {
+ if (hasChildren) {
+ setSelectedParent(emotion);
+ } else {
+ onChange(emotion.id);
+ }
+ }}
+ className={`
+ flex flex-col items-center gap-2 p-4 rounded-xl border-2 transition-all duration-200
+ ${isSelected
+ ? 'shadow-lg scale-105'
+ : 'border-gray-200 dark:border-gray-700 hover:shadow-md'
+ }
+ `}
+ style={{
+ borderColor: isSelected ? emotion.couleur : undefined,
+ }}
+ >
+ {emotion.icone || '🔵'}
+ {emotion.nom}
+ {hasChildren && (
+ ▼
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/frontend/src/components/features/TrackerTimeline.tsx b/frontend/src/components/features/TrackerTimeline.tsx
new file mode 100644
index 0000000..61dc629
--- /dev/null
+++ b/frontend/src/components/features/TrackerTimeline.tsx
@@ -0,0 +1,113 @@
+'use client';
+
+import React from 'react';
+import type { SaisieTracker } from '@/types';
+import Badge from '@/components/ui/Badge';
+import { Trash2, Edit2 } from 'lucide-react';
+
+interface TrackerTimelineProps {
+ saisies: SaisieTracker[];
+ onEdit?: (saisie: SaisieTracker) => void;
+ onDelete?: (id: number) => void;
+}
+
+export default function TrackerTimeline({ saisies, onEdit, onDelete }: TrackerTimelineProps) {
+ if (saisies.length === 0) {
+ return (
+
+
Aucune saisie enregistrée
+
Commencez par enregistrer votre première émotion
+
+ );
+ }
+
+ // Grouper les saisies par date
+ const grouped = saisies.reduce>((acc, saisie) => {
+ const date = new Date(saisie.date_saisie).toLocaleDateString('fr-FR', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ });
+ if (!acc[date]) acc[date] = [];
+ acc[date].push(saisie);
+ return acc;
+ }, {});
+
+ return (
+
+ {Object.entries(grouped).map(([date, items]) => (
+
+
+ {date}
+
+
+ {items.map((saisie) => (
+
+ {/* Icône émotion */}
+
+ {saisie.emotion?.icone || '🔵'}
+
+
+ {/* Contenu */}
+
+
+
+ {saisie.emotion?.nom}
+
+
+ Intensité : {saisie.intensite}/10
+
+
+ {/* Barre d'intensité */}
+
+ {saisie.note && (
+
+ {saisie.note}
+
+ )}
+
+
+ {/* Actions */}
+
+ {onEdit && (
+ onEdit(saisie)}
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
+ aria-label="Modifier"
+ >
+
+
+ )}
+ {onDelete && (
+ onDelete(saisie.id)}
+ className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/30 text-gray-400 hover:text-red-500 transition-colors"
+ aria-label="Supprimer"
+ >
+
+
+ )}
+
+
+ ))}
+
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx
new file mode 100644
index 0000000..72adba8
--- /dev/null
+++ b/frontend/src/components/layout/Footer.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { Phone } from "lucide-react";
+
+export default function Footer() {
+ return (
+
+ {/* Bandeau urgence */}
+
+
+
+
+ En cas d'urgence : 3114 (numéro national de prévention du suicide)
+
+
+
+
+
+
+ {/* Logo + Copyright */}
+
+
+
+ CESI
+
+
+ ZEN
+
+
+
+ CESIZen 2024 - Ministère des Solidarités et de la Santé
+
+
+
+ {/* Links */}
+
+
+ Mentions légales
+
+
+ Politique de confidentialité
+
+
+ Contacts d'urgence
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx
new file mode 100644
index 0000000..32a106d
--- /dev/null
+++ b/frontend/src/components/layout/Header.tsx
@@ -0,0 +1,245 @@
+"use client";
+
+import React, { useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ Menu,
+ X,
+ User,
+ LogOut,
+ Settings,
+ BarChart3,
+ BookOpen,
+ Home,
+} from "lucide-react";
+import ThemeToggle from "@/components/ui/ThemeToggle";
+import { useAuthStore } from "@/lib/auth-store";
+
+interface NavLink {
+ href: string;
+ label: string;
+ icon: React.ReactNode;
+}
+
+export default function Header() {
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [userDropdownOpen, setUserDropdownOpen] = useState(false);
+ const pathname = usePathname();
+
+ const { user, token, logout } = useAuthStore();
+ const isAuthenticated = !!token;
+ const isAdmin = user?.role?.nom === "administrateur";
+
+ const publicLinks: NavLink[] = [
+ { href: "/", label: "Accueil", icon: },
+ { href: "/informations", label: "Informations", icon: },
+ ];
+
+ const authLinks: NavLink[] = [
+ { href: "/journal", label: "Journal", icon: },
+ { href: "/journal/rapports", label: "Rapports", icon: },
+ ];
+
+ const adminLinks: NavLink[] = [
+ { href: "/admin", label: "Admin", icon: },
+ ];
+
+ const navLinks = [
+ ...publicLinks,
+ ...(isAuthenticated ? authLinks : []),
+ ...(isAdmin ? adminLinks : []),
+ ];
+
+ const isActive = (href: string) => {
+ if (href === "/") return pathname === "/";
+ return pathname.startsWith(href);
+ };
+
+ const handleLogout = () => {
+ logout();
+ setUserDropdownOpen(false);
+ setMobileMenuOpen(false);
+ };
+
+ return (
+
+
+
+ {/* Logo */}
+
+
+ CESI
+
+
+ ZEN
+
+
+
+ {/* Desktop Navigation */}
+
+ {navLinks.map((link) => (
+
+ {link.icon}
+ {link.label}
+
+ ))}
+
+
+ {/* Right actions */}
+
+
+
+ {/* Desktop auth */}
+
+ {isAuthenticated ? (
+
+
setUserDropdownOpen(!userDropdownOpen)}
+ className="flex items-center gap-2 px-3 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
+ >
+
+
+
+
+ {user?.prenom || "Utilisateur"}
+
+
+
+ {userDropdownOpen && (
+ <>
+
setUserDropdownOpen(false)}
+ />
+
+ setUserDropdownOpen(false)}
+ className="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
+ >
+
+ Mon profil
+
+
+
+ Déconnexion
+
+
+ >
+ )}
+
+ ) : (
+ <>
+
+ Connexion
+
+
+ Inscription
+
+ >
+ )}
+
+
+ {/* Mobile hamburger */}
+
setMobileMenuOpen(!mobileMenuOpen)}
+ className="md:hidden p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-gray-600 dark:text-gray-400"
+ aria-label="Menu"
+ >
+ {mobileMenuOpen ? : }
+
+
+
+
+
+ {/* Mobile menu */}
+ {mobileMenuOpen && (
+
+
+ {navLinks.map((link) => (
+ setMobileMenuOpen(false)}
+ className={`
+ flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200
+ ${
+ isActive(link.href)
+ ? "bg-candlelight-100 dark:bg-candlelight-950 text-candlelight-700 dark:text-candlelight-400"
+ : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
+ }
+ `}
+ >
+ {link.icon}
+ {link.label}
+
+ ))}
+
+
+ {isAuthenticated ? (
+ <>
+ setMobileMenuOpen(false)}
+ className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
+ >
+
+ Mon profil
+
+
+
+ Déconnexion
+
+ >
+ ) : (
+ <>
+ setMobileMenuOpen(false)}
+ className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
+ >
+ Connexion
+
+ setMobileMenuOpen(false)}
+ className="flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-semibold bg-candlelight-500 hover:bg-candlelight-600 text-black transition-all duration-200"
+ >
+ Inscription
+
+ >
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..02b6933
--- /dev/null
+++ b/frontend/src/components/layout/Sidebar.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import React, { useState } from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import {
+ LayoutDashboard,
+ Users,
+ FileText,
+ Heart,
+ Menu,
+ X,
+} from "lucide-react";
+
+interface SidebarLink {
+ href: string;
+ label: string;
+ icon: React.ReactNode;
+}
+
+const sidebarLinks: SidebarLink[] = [
+ {
+ href: "/admin",
+ label: "Dashboard",
+ icon:
,
+ },
+ {
+ href: "/admin/utilisateurs",
+ label: "Utilisateurs",
+ icon:
,
+ },
+ {
+ href: "/admin/contenus",
+ label: "Contenus",
+ icon:
,
+ },
+ {
+ href: "/admin/emotions",
+ label: "Émotions",
+ icon:
,
+ },
+];
+
+export default function Sidebar() {
+ const [collapsed, setCollapsed] = useState(false);
+ const pathname = usePathname();
+
+ const isActive = (href: string) => {
+ if (href === "/admin") return pathname === "/admin";
+ return pathname.startsWith(href);
+ };
+
+ return (
+ <>
+ {/* Mobile toggle */}
+
setCollapsed(!collapsed)}
+ className="lg:hidden fixed top-20 left-4 z-40 p-2 bg-white dark:bg-gray-900 rounded-xl shadow-lg border border-gray-200 dark:border-gray-800 text-gray-600 dark:text-gray-400"
+ aria-label="Toggle sidebar"
+ >
+ {collapsed ? : }
+
+
+ {/* Overlay mobile */}
+ {collapsed && (
+
setCollapsed(false)}
+ />
+ )}
+
+ {/* Sidebar */}
+
+ {/* Logo */}
+
+
+
+ CESI
+
+
+ ZEN
+
+
+ setCollapsed(false)}
+ className="lg:hidden p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
+ >
+
+
+
+
+ {/* Navigation */}
+
+ {sidebarLinks.map((link) => (
+ setCollapsed(false)}
+ className={`
+ flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200
+ ${
+ isActive(link.href)
+ ? "bg-candlelight-100 dark:bg-candlelight-950 text-candlelight-700 dark:text-candlelight-400 shadow-sm"
+ : "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-gray-200"
+ }
+ `}
+ >
+ {link.icon}
+ {link.label}
+
+ ))}
+
+
+ {/* Footer */}
+
+
+ Administration CESIZen
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/components/ui/Badge.tsx b/frontend/src/components/ui/Badge.tsx
new file mode 100644
index 0000000..3949949
--- /dev/null
+++ b/frontend/src/components/ui/Badge.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React from "react";
+
+interface BadgeProps {
+ children: React.ReactNode;
+ color?: string;
+ className?: string;
+}
+
+export default function Badge({
+ children,
+ color = "#fce117",
+ className = "",
+}: BadgeProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/components/ui/Button.tsx b/frontend/src/components/ui/Button.tsx
new file mode 100644
index 0000000..b329d08
--- /dev/null
+++ b/frontend/src/components/ui/Button.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import React from "react";
+
+type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
+type ButtonSize = "sm" | "md" | "lg";
+
+interface ButtonProps extends React.ButtonHTMLAttributes
{
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ loading?: boolean;
+}
+
+const variantClasses: Record = {
+ primary:
+ "bg-candlelight-500 hover:bg-candlelight-600 text-black font-semibold shadow-sm hover:shadow-md",
+ secondary:
+ "bg-malachite-500 hover:bg-malachite-600 text-white font-semibold shadow-sm hover:shadow-md",
+ outline:
+ "border-2 border-candlelight-500 text-candlelight-700 dark:text-candlelight-400 hover:bg-candlelight-50 dark:hover:bg-candlelight-950",
+ ghost:
+ "hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300",
+ danger:
+ "bg-red-500 hover:bg-red-600 text-white font-semibold shadow-sm hover:shadow-md",
+};
+
+const sizeClasses: Record = {
+ sm: "px-3 py-1.5 text-sm",
+ md: "px-5 py-2.5 text-base",
+ lg: "px-7 py-3.5 text-lg",
+};
+
+export default function Button({
+ variant = "primary",
+ size = "md",
+ loading = false,
+ disabled,
+ children,
+ className = "",
+ ...rest
+}: ButtonProps) {
+ return (
+
+ {loading && (
+
+
+
+
+ )}
+ {children}
+
+ );
+}
diff --git a/frontend/src/components/ui/Card.tsx b/frontend/src/components/ui/Card.tsx
new file mode 100644
index 0000000..01be35b
--- /dev/null
+++ b/frontend/src/components/ui/Card.tsx
@@ -0,0 +1,27 @@
+"use client";
+
+import React from "react";
+
+interface CardProps {
+ children: React.ReactNode;
+ className?: string;
+ hover?: boolean;
+}
+
+export default function Card({ children, className = "", hover = false }: CardProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/src/components/ui/Input.tsx b/frontend/src/components/ui/Input.tsx
new file mode 100644
index 0000000..52e4743
--- /dev/null
+++ b/frontend/src/components/ui/Input.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import React from "react";
+
+interface InputProps extends React.InputHTMLAttributes {
+ label?: string;
+ error?: string;
+ icon?: React.ReactNode;
+}
+
+export default function Input({
+ label,
+ error,
+ icon,
+ className = "",
+ id,
+ ...inputProps
+}: InputProps) {
+ const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
+
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {icon && (
+
+ {icon}
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/ui/Modal.tsx b/frontend/src/components/ui/Modal.tsx
new file mode 100644
index 0000000..ac4a548
--- /dev/null
+++ b/frontend/src/components/ui/Modal.tsx
@@ -0,0 +1,89 @@
+"use client";
+
+import React, { useEffect, useCallback } from "react";
+import { X } from "lucide-react";
+
+type ModalSize = "sm" | "md" | "lg";
+
+interface ModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ title?: string;
+ children: React.ReactNode;
+ size?: ModalSize;
+}
+
+const sizeClasses: Record = {
+ sm: "max-w-sm",
+ md: "max-w-lg",
+ lg: "max-w-2xl",
+};
+
+export default function Modal({
+ isOpen,
+ onClose,
+ title,
+ children,
+ size = "md",
+}: ModalProps) {
+ const handleKeyDown = useCallback(
+ (e: KeyboardEvent) => {
+ if (e.key === "Escape") onClose();
+ },
+ [onClose]
+ );
+
+ useEffect(() => {
+ if (isOpen) {
+ document.addEventListener("keydown", handleKeyDown);
+ document.body.style.overflow = "hidden";
+ }
+ return () => {
+ document.removeEventListener("keydown", handleKeyDown);
+ document.body.style.overflow = "";
+ };
+ }, [isOpen, handleKeyDown]);
+
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Overlay */}
+
+
+ {/* Modal */}
+
+ {/* Header */}
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+
+
+
+ {/* Content */}
+
{children}
+
+
+ );
+}
diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx
new file mode 100644
index 0000000..6e2fa6f
--- /dev/null
+++ b/frontend/src/components/ui/Skeleton.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import React from "react";
+
+type SkeletonVariant = "text" | "circle" | "rect";
+
+interface SkeletonProps {
+ className?: string;
+ variant?: SkeletonVariant;
+}
+
+const variantClasses: Record = {
+ text: "h-4 w-full rounded",
+ circle: "h-10 w-10 rounded-full",
+ rect: "h-24 w-full rounded-xl",
+};
+
+export default function Skeleton({
+ className = "",
+ variant = "text",
+}: SkeletonProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/ui/Slider.tsx b/frontend/src/components/ui/Slider.tsx
new file mode 100644
index 0000000..dd2287e
--- /dev/null
+++ b/frontend/src/components/ui/Slider.tsx
@@ -0,0 +1,52 @@
+"use client";
+
+import React from "react";
+
+interface SliderProps {
+ value: number;
+ onChange: (value: number) => void;
+ min?: number;
+ max?: number;
+ label?: string;
+ showValue?: boolean;
+}
+
+export default function Slider({
+ value,
+ onChange,
+ min = 1,
+ max = 10,
+ label,
+ showValue = true,
+}: SliderProps) {
+ return (
+
+ {(label || showValue) && (
+
+ {label && (
+
+ {label}
+
+ )}
+ {showValue && (
+
+ {value}
+
+ )}
+
+ )}
+
onChange(Number(e.target.value))}
+ className="w-full h-2 rounded-full appearance-none cursor-pointer bg-gray-200 dark:bg-gray-700 accent-candlelight-500"
+ />
+
+ {min}
+ {max}
+
+
+ );
+}
diff --git a/frontend/src/components/ui/ThemeToggle.tsx b/frontend/src/components/ui/ThemeToggle.tsx
new file mode 100644
index 0000000..a6ad315
--- /dev/null
+++ b/frontend/src/components/ui/ThemeToggle.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import React from "react";
+import { useTheme } from "next-themes";
+import { Sun, Moon } from "lucide-react";
+
+export default function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+ const [mounted, setMounted] = React.useState(false);
+
+ React.useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ setTheme(theme === "dark" ? "light" : "dark")}
+ className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-200 text-gray-600 dark:text-gray-400"
+ aria-label={theme === "dark" ? "Passer en mode clair" : "Passer en mode sombre"}
+ >
+ {theme === "dark" ? : }
+
+ );
+}
diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts
new file mode 100644
index 0000000..40151e0
--- /dev/null
+++ b/frontend/src/hooks/useAuth.ts
@@ -0,0 +1,12 @@
+import { useAuthStore } from '@/lib/auth-store';
+
+export function useAuth() {
+ const store = useAuthStore();
+
+ const isAdmin = store.user?.role?.nom === 'administrateur';
+
+ return {
+ ...store,
+ isAdmin,
+ };
+}
diff --git a/frontend/src/hooks/useEmotions.ts b/frontend/src/hooks/useEmotions.ts
new file mode 100644
index 0000000..d64cffd
--- /dev/null
+++ b/frontend/src/hooks/useEmotions.ts
@@ -0,0 +1,13 @@
+import { useQuery } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { Emotion } from '@/types';
+
+export function useEmotions() {
+ return useQuery({
+ queryKey: ['emotions'],
+ queryFn: async () => {
+ const response = await api.get('/emotions');
+ return response.data;
+ },
+ });
+}
diff --git a/frontend/src/hooks/useFeeds.ts b/frontend/src/hooks/useFeeds.ts
new file mode 100644
index 0000000..ee7ac96
--- /dev/null
+++ b/frontend/src/hooks/useFeeds.ts
@@ -0,0 +1,24 @@
+import { useQuery } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { Feed } from '@/types';
+
+export function useFeeds() {
+ return useQuery({
+ queryKey: ['feeds'],
+ queryFn: async () => {
+ const response = await api.get('/feeds');
+ return response.data;
+ },
+ });
+}
+
+export function useFeed(slug: string) {
+ return useQuery({
+ queryKey: ['feeds', slug],
+ queryFn: async () => {
+ const response = await api.get(`/feeds/${slug}`);
+ return response.data;
+ },
+ enabled: !!slug,
+ });
+}
diff --git a/frontend/src/hooks/useRapport.ts b/frontend/src/hooks/useRapport.ts
new file mode 100644
index 0000000..fdce8b9
--- /dev/null
+++ b/frontend/src/hooks/useRapport.ts
@@ -0,0 +1,16 @@
+import { useQuery } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { RapportData } from '@/types';
+
+export function useRapport(period: 'week' | 'month' | 'quarter' | 'year') {
+ return useQuery({
+ queryKey: ['rapport', period],
+ queryFn: async () => {
+ const response = await api.get('/tracker/rapports', {
+ params: { period },
+ });
+ return response.data;
+ },
+ enabled: !!period,
+ });
+}
diff --git a/frontend/src/hooks/useTracker.ts b/frontend/src/hooks/useTracker.ts
new file mode 100644
index 0000000..8b47259
--- /dev/null
+++ b/frontend/src/hooks/useTracker.ts
@@ -0,0 +1,75 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { SaisieTracker } from '@/types';
+
+interface SaisieFilters {
+ date_debut?: string;
+ date_fin?: string;
+ emotion_id?: number;
+}
+
+export function useSaisies(filters?: SaisieFilters) {
+ return useQuery({
+ queryKey: ['saisies', filters],
+ queryFn: async () => {
+ const response = await api.get('/tracker/saisies', { params: filters });
+ return response.data.data ?? response.data;
+ },
+ });
+}
+
+export function useCreateSaisie() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (data: {
+ emotion_id: number;
+ intensite: number;
+ note?: string;
+ date_saisie?: string;
+ }) => {
+ const response = await api.post('/tracker/saisies', data);
+ return response.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['saisies'] });
+ },
+ });
+}
+
+export function useUpdateSaisie() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({
+ id,
+ ...data
+ }: {
+ id: number;
+ emotion_id?: number;
+ intensite?: number;
+ note?: string;
+ date_saisie?: string;
+ }) => {
+ const response = await api.put(`/tracker/saisies/${id}`, data);
+ return response.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['saisies'] });
+ },
+ });
+}
+
+export function useDeleteSaisie() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (id: number) => {
+ const response = await api.delete(`/tracker/saisies/${id}`);
+ return response.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['saisies'] });
+ },
+ });
+}
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
new file mode 100644
index 0000000..23f6702
--- /dev/null
+++ b/frontend/src/lib/api.ts
@@ -0,0 +1,41 @@
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ },
+});
+
+// Intercepteur de requête : ajouter le token JWT si présent
+api.interceptors.request.use(
+ (config) => {
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('cesizen_token');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ }
+ return config;
+ },
+ (error) => Promise.reject(error)
+);
+
+// Intercepteur de réponse : gérer les erreurs 401
+api.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (
+ error.response?.status === 401 &&
+ typeof window !== 'undefined' &&
+ !window.location.pathname.includes('/connexion')
+ ) {
+ localStorage.removeItem('cesizen_token');
+ window.location.href = '/connexion';
+ }
+ return Promise.reject(error);
+ }
+);
+
+export default api;
diff --git a/frontend/src/lib/auth-store.ts b/frontend/src/lib/auth-store.ts
new file mode 100644
index 0000000..087e167
--- /dev/null
+++ b/frontend/src/lib/auth-store.ts
@@ -0,0 +1,103 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+import api from './api';
+import type { Utilisateur } from '@/types';
+
+function syncCookie(user: Utilisateur | null, token: string | null) {
+ if (typeof document === 'undefined') return;
+ if (token) {
+ const value = JSON.stringify({
+ state: {
+ token,
+ isAuthenticated: true,
+ user: user ? { role: { nom: user.role?.nom } } : null,
+ },
+ });
+ document.cookie = `cesizen-auth=${encodeURIComponent(value)};path=/;max-age=${60 * 60 * 24 * 7};SameSite=Lax`;
+ } else {
+ document.cookie = 'cesizen-auth=;path=/;max-age=0';
+ }
+}
+
+interface AuthState {
+ user: Utilisateur | null;
+ token: string | null;
+ isAuthenticated: boolean;
+ login: (email: string, password: string) => Promise;
+ register: (data: {
+ nom: string;
+ prenom: string;
+ email: string;
+ password: string;
+ password_confirmation: string;
+ }) => Promise;
+ logout: () => Promise;
+ fetchUser: () => Promise;
+ setAuth: (user: Utilisateur | null, token: string | null) => void;
+}
+
+export const useAuthStore = create()(
+ persist(
+ (set, get) => ({
+ user: null,
+ token: null,
+ isAuthenticated: false,
+
+ login: async (email: string, password: string) => {
+ const response = await api.post('/auth/login', { email, password });
+ const { utilisateur, token } = response.data;
+ localStorage.setItem('cesizen_token', token);
+ syncCookie(utilisateur, token);
+ set({ user: utilisateur, token, isAuthenticated: true });
+ },
+
+ register: async (data) => {
+ const response = await api.post('/auth/register', data);
+ const { utilisateur, token } = response.data;
+ localStorage.setItem('cesizen_token', token);
+ syncCookie(utilisateur, token);
+ set({ user: utilisateur, token, isAuthenticated: true });
+ },
+
+ logout: async () => {
+ try {
+ await api.post('/auth/logout');
+ } catch {
+ // Ignorer les erreurs de déconnexion
+ }
+ localStorage.removeItem('cesizen_token');
+ syncCookie(null, null);
+ set({ user: null, token: null, isAuthenticated: false });
+ },
+
+ fetchUser: async () => {
+ const response = await api.get('/auth/me');
+ set({ user: response.data });
+ },
+
+ setAuth: (user: Utilisateur | null, token: string | null) => {
+ if (token) {
+ localStorage.setItem('cesizen_token', token);
+ } else {
+ localStorage.removeItem('cesizen_token');
+ }
+ set({ user, token, isAuthenticated: !!token });
+ },
+ }),
+ {
+ name: 'cesizen-auth',
+ partialize: (state) => ({
+ user: state.user,
+ token: state.token,
+ isAuthenticated: state.isAuthenticated,
+ }),
+ onRehydrateStorage: () => {
+ return (state) => {
+ if (state) {
+ syncCookie(state.user, state.token);
+ }
+ };
+ },
+ }
+ )
+);
diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts
new file mode 100644
index 0000000..b6c7a7d
--- /dev/null
+++ b/frontend/src/middleware.ts
@@ -0,0 +1,72 @@
+import { NextResponse } from 'next/server';
+import type { NextRequest } from 'next/server';
+
+const protectedRoutes = ['/dashboard', '/profil', '/journal'];
+const adminRoutes = ['/admin'];
+const authRoutes = ['/connexion', '/inscription'];
+
+export function middleware(request: NextRequest) {
+ const { pathname } = request.nextUrl;
+
+ // Vérifier si un token existe dans les cookies de Zustand persist (localStorage côté client)
+ // En middleware Next.js (edge), on ne peut pas lire localStorage.
+ // On utilise un cookie pour transmettre l'état d'auth.
+ const authCookie = request.cookies.get('cesizen-auth');
+ let isAuthenticated = false;
+ let isAdmin = false;
+
+ if (authCookie?.value) {
+ try {
+ const parsed = JSON.parse(authCookie.value);
+ const state = parsed?.state;
+ isAuthenticated = !!state?.token;
+ isAdmin = state?.user?.role?.nom === 'administrateur';
+ } catch {
+ // Cookie invalide
+ }
+ }
+
+ // Pages auth (connexion/inscription) : rediriger si déjà connecté
+ if (authRoutes.some((route) => pathname.startsWith(route))) {
+ if (isAuthenticated) {
+ return NextResponse.redirect(new URL('/dashboard', request.url));
+ }
+ return NextResponse.next();
+ }
+
+ // Routes protégées : rediriger si pas connecté
+ if (protectedRoutes.some((route) => pathname.startsWith(route))) {
+ if (!isAuthenticated) {
+ const url = new URL('/connexion', request.url);
+ url.searchParams.set('redirect', pathname);
+ return NextResponse.redirect(url);
+ }
+ return NextResponse.next();
+ }
+
+ // Routes admin : vérifier rôle admin
+ if (adminRoutes.some((route) => pathname.startsWith(route))) {
+ if (!isAuthenticated) {
+ const url = new URL('/connexion', request.url);
+ url.searchParams.set('redirect', pathname);
+ return NextResponse.redirect(url);
+ }
+ if (!isAdmin) {
+ return NextResponse.redirect(new URL('/dashboard', request.url));
+ }
+ return NextResponse.next();
+ }
+
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: [
+ '/dashboard/:path*',
+ '/profil/:path*',
+ '/journal/:path*',
+ '/admin/:path*',
+ '/connexion',
+ '/inscription',
+ ],
+};
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
new file mode 100644
index 0000000..e71be41
--- /dev/null
+++ b/frontend/src/types/index.ts
@@ -0,0 +1,95 @@
+export interface Role {
+ id: number;
+ nom: string;
+ description: string | null;
+}
+
+export interface Utilisateur {
+ id: number;
+ nom: string;
+ prenom: string;
+ email: string;
+ role_id: number;
+ role: Role;
+ est_actif: boolean;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface Emotion {
+ id: number;
+ nom: string;
+ couleur: string;
+ icone: string | null;
+ niveau: number;
+ parent_id: number | null;
+ est_actif: boolean;
+ enfants?: Emotion[];
+}
+
+export interface SaisieTracker {
+ id: number;
+ tracker_id: number;
+ emotion_id: number;
+ emotion: Emotion;
+ intensite: number;
+ note: string | null;
+ date_saisie: string;
+ created_at: string;
+}
+
+export interface Feed {
+ id: number;
+ titre: string;
+ slug: string;
+ contenu: string;
+ image_url: string | null;
+ est_publie: boolean;
+ auteur_id: number;
+ auteur?: { id: number; nom: string; prenom: string };
+ ordre: number;
+ created_at: string;
+}
+
+export interface RapportStats {
+ total_saisies: number;
+ intensite_moyenne: number;
+ emotion_dominante: {
+ nom: string;
+ couleur: string;
+ icone: string | null;
+ count: number;
+ } | null;
+}
+
+export interface RapportData {
+ periode: string;
+ date_debut: string;
+ date_fin: string;
+ stats: RapportStats;
+ repartition: {
+ nom: string;
+ couleur: string;
+ icone: string | null;
+ count: number;
+ pourcentage: number;
+ intensite_moyenne: number;
+ }[];
+ evolution: {
+ labels: string[];
+ intensite_moyenne: number[];
+ emotions: {
+ nom: string;
+ couleur: string;
+ data: number[];
+ }[];
+ };
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ current_page: number;
+ last_page: number;
+ per_page: number;
+ total: number;
+}
diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts
new file mode 100644
index 0000000..eeba113
--- /dev/null
+++ b/frontend/tailwind.config.ts
@@ -0,0 +1,49 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: [
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+ ],
+ darkMode: "class",
+ theme: {
+ extend: {
+ colors: {
+ candlelight: {
+ 50: "#fffde6",
+ 100: "#fffacc",
+ 200: "#fff599",
+ 300: "#ffef66",
+ 400: "#ffe833",
+ 500: "#fce117",
+ 600: "#e5c800",
+ 700: "#b39c00",
+ 800: "#806f00",
+ 900: "#4d4300",
+ 950: "#332d00",
+ },
+ malachite: {
+ 50: "#e6fdf0",
+ 100: "#ccfbe0",
+ 200: "#99f7c2",
+ 300: "#66f3a3",
+ 400: "#33ef85",
+ 500: "#06c656",
+ 600: "#05a348",
+ 700: "#047d37",
+ 800: "#035827",
+ 900: "#023316",
+ 950: "#01220f",
+ },
+ },
+ fontFamily: {
+ sans: ["Inter", "system-ui", "sans-serif"],
+ display: ["Poppins", "system-ui", "sans-serif"],
+ },
+ },
+ },
+ plugins: [],
+};
+
+export default config;
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..cf9c65d
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts",
+ "**/*.mts"
+ ],
+ "exclude": ["node_modules"]
+}