feat(frontend): convertit l'ensemble des pages et composants au DSFR
- composants UI (Input, Card, Badge, Modal, ConfirmDialog, Slider, Skeleton, ThemeToggle) + Sidebar - pages auth (connexion, inscription) - pages publiques (informations, détail, contacts urgence, mentions, RGPD) - espace membre (dashboard, journal, wizard saisie via fr-stepper, rapports, profil) + composants tracker (EditSaisieModal, EmotionPicker, TrackerTimeline, EmotionChart) - back-office admin (dashboard, utilisateurs, contenus, émotions) Suppression de Tailwind candlelight/malachite + lucide-react. Build OK (20 routes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,12 +2,9 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, ArrowRight, Check } from 'lucide-react';
|
||||
import { useCreateSaisie } from '@/hooks/useTracker';
|
||||
import EmotionPicker from '@/components/features/EmotionPicker';
|
||||
import Slider from '@/components/ui/Slider';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const STEPS = [
|
||||
@@ -49,139 +46,144 @@ export default function NouvelleSaisiePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const nextStep = STEPS[step]?.title;
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 mb-6 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Retour
|
||||
</button>
|
||||
<div className="fr-container fr-py-6w">
|
||||
<div className="fr-grid-row fr-grid-row--center">
|
||||
<div className="fr-col-12 fr-col-md-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
className="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-icon-arrow-left-line fr-btn--icon-left fr-mb-2w"
|
||||
>
|
||||
Retour
|
||||
</button>
|
||||
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Nouvelle saisie
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-8">
|
||||
Enregistrez votre émotion en quelques étapes simples.
|
||||
</p>
|
||||
<h1>Nouvelle saisie</h1>
|
||||
<p className="fr-text--lead">
|
||||
Enregistrez votre émotion en quelques étapes simples.
|
||||
</p>
|
||||
|
||||
{/* Stepper */}
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
{STEPS.map((s) => (
|
||||
<div key={s.id} className="flex items-center gap-2 flex-1">
|
||||
{/* Stepper DSFR */}
|
||||
<div className="fr-stepper">
|
||||
<h2 className="fr-stepper__title">
|
||||
{STEPS[step - 1].title}
|
||||
<span className="fr-stepper__state">
|
||||
Étape {step} sur {STEPS.length}
|
||||
</span>
|
||||
</h2>
|
||||
<div
|
||||
className={`
|
||||
w-8 h-8 rounded-full flex items-center justify-center text-sm font-semibold shrink-0
|
||||
${step > s.id
|
||||
? 'bg-malachite-500 text-white'
|
||||
: step === s.id
|
||||
? 'bg-candlelight-500 text-black'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{step > s.id ? <Check size={16} /> : s.id}
|
||||
</div>
|
||||
{s.id < STEPS.length && (
|
||||
<div
|
||||
className={`flex-1 h-0.5 ${
|
||||
step > s.id ? 'bg-malachite-500' : 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
className="fr-stepper__steps"
|
||||
data-fr-current-step={step}
|
||||
data-fr-steps={STEPS.length}
|
||||
/>
|
||||
{nextStep && (
|
||||
<p className="fr-stepper__details">
|
||||
<span className="fr-text--bold">Étape suivante :</span> {nextStep}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Étape actuelle */}
|
||||
<Card className="mb-6">
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{STEPS[step - 1].title}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
||||
{STEPS[step - 1].description}
|
||||
</p>
|
||||
{/* Étape actuelle */}
|
||||
<div className="fr-card fr-p-3w fr-mb-3w">
|
||||
<p className="fr-text--sm fr-text-mention--grey fr-mb-3w">
|
||||
{STEPS[step - 1].description}
|
||||
</p>
|
||||
|
||||
{step === 1 && (
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
)}
|
||||
{step === 1 && (
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-6">
|
||||
<Slider
|
||||
value={intensite}
|
||||
onChange={setIntensite}
|
||||
min={1}
|
||||
max={10}
|
||||
label="Intensité de l'émotion"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>Faible</span>
|
||||
<span>Modérée</span>
|
||||
<span>Forte</span>
|
||||
</div>
|
||||
{step === 2 && (
|
||||
<div>
|
||||
<Slider
|
||||
value={intensite}
|
||||
onChange={setIntensite}
|
||||
min={1}
|
||||
max={10}
|
||||
label="Intensité de l'émotion"
|
||||
/>
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-mt-1w">
|
||||
<span className="fr-col fr-text--sm">Faible</span>
|
||||
<span className="fr-col fr-text--sm" style={{ textAlign: 'center' }}>
|
||||
Modérée
|
||||
</span>
|
||||
<span className="fr-col fr-text--sm" style={{ textAlign: 'right' }}>
|
||||
Forte
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div>
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="saisie-date">
|
||||
Date de la saisie
|
||||
</label>
|
||||
<input
|
||||
id="saisie-date"
|
||||
className="fr-input"
|
||||
type="date"
|
||||
value={dateSaisie}
|
||||
onChange={(e) => setDateSaisie(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="saisie-note">
|
||||
Note (optionnel)
|
||||
</label>
|
||||
<textarea
|
||||
id="saisie-note"
|
||||
className="fr-input"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion..."
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Date de la saisie
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={dateSaisie}
|
||||
onChange={(e) => setDateSaisie(e.target.value)}
|
||||
className="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-candlelight-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Note (optionnel)
|
||||
</label>
|
||||
<textarea
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion..."
|
||||
rows={4}
|
||||
className="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-candlelight-500 placeholder:text-gray-400 resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setStep(step - 1)}
|
||||
disabled={step === 1}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Précédent
|
||||
</Button>
|
||||
|
||||
{step < 3 ? (
|
||||
<Button
|
||||
onClick={() => setStep(step + 1)}
|
||||
disabled={!canNext()}
|
||||
>
|
||||
Suivant
|
||||
<ArrowRight size={16} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={createSaisie.isPending}
|
||||
disabled={!emotionId}
|
||||
>
|
||||
<Check size={16} />
|
||||
Enregistrer
|
||||
</Button>
|
||||
)}
|
||||
{/* Navigation */}
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--secondary fr-icon-arrow-left-line fr-btn--icon-left"
|
||||
onClick={() => setStep(step - 1)}
|
||||
disabled={step === 1}
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
</li>
|
||||
{step < 3 ? (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-icon-arrow-right-line fr-btn--icon-right"
|
||||
onClick={() => setStep(step + 1)}
|
||||
disabled={!canNext()}
|
||||
>
|
||||
Suivant
|
||||
</button>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-icon-check-line fr-btn--icon-left"
|
||||
onClick={handleSubmit}
|
||||
disabled={!emotionId || createSaisie.isPending}
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,10 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Plus, Filter, Download } from 'lucide-react';
|
||||
import { useSaisies, useDeleteSaisie } from '@/hooks/useTracker';
|
||||
import { useEmotions } from '@/hooks/useEmotions';
|
||||
import TrackerTimeline from '@/components/features/TrackerTimeline';
|
||||
import EditSaisieModal from '@/components/features/EditSaisieModal';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Skeleton from '@/components/ui/Skeleton';
|
||||
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||
import toast from 'react-hot-toast';
|
||||
import type { SaisieTracker } from '@/types';
|
||||
@@ -71,107 +67,126 @@ export default function JournalPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
Mon journal
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
<div className="fr-container fr-py-6w">
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-mb-4w">
|
||||
<div className="fr-col-12 fr-col-md">
|
||||
<h1 className="fr-mb-1v">Mon journal</h1>
|
||||
<p className="fr-text--lead fr-mb-0">
|
||||
Historique de vos émotions au quotidien
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
<Filter size={16} />
|
||||
Filtrer
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={handleExport} title="Exporter mes saisies (RGPD)">
|
||||
<Download size={16} />
|
||||
Export CSV
|
||||
</Button>
|
||||
<Link href="/journal/nouvelle-saisie">
|
||||
<Button size="sm">
|
||||
<Plus size={16} />
|
||||
Nouvelle saisie
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="fr-col-12 fr-col-md--right">
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md fr-btns-group--right">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--tertiary fr-btn--sm fr-icon-filter-line fr-btn--icon-left"
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
>
|
||||
Filtrer
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--tertiary fr-btn--sm fr-icon-download-line fr-btn--icon-left"
|
||||
onClick={handleExport}
|
||||
title="Exporter mes saisies (RGPD)"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/journal/nouvelle-saisie"
|
||||
className="fr-btn fr-btn--sm fr-icon-add-line fr-btn--icon-left"
|
||||
>
|
||||
Nouvelle saisie
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtres */}
|
||||
{showFilters && (
|
||||
<Card className="mb-6">
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Date début
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.date_debut || ''}
|
||||
onChange={(e) => setFilters({ ...filters, date_debut: e.target.value || undefined })}
|
||||
className="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-candlelight-500"
|
||||
/>
|
||||
<div className="fr-card fr-p-3w fr-mb-4w">
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="filtre-date-debut">
|
||||
Date début
|
||||
</label>
|
||||
<input
|
||||
id="filtre-date-debut"
|
||||
className="fr-input"
|
||||
type="date"
|
||||
value={filters.date_debut || ''}
|
||||
onChange={(e) =>
|
||||
setFilters({ ...filters, date_debut: e.target.value || undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Date fin
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.date_fin || ''}
|
||||
onChange={(e) => setFilters({ ...filters, date_fin: e.target.value || undefined })}
|
||||
className="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-candlelight-500"
|
||||
/>
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="filtre-date-fin">
|
||||
Date fin
|
||||
</label>
|
||||
<input
|
||||
id="filtre-date-fin"
|
||||
className="fr-input"
|
||||
type="date"
|
||||
value={filters.date_fin || ''}
|
||||
onChange={(e) =>
|
||||
setFilters({ ...filters, date_fin: e.target.value || undefined })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Émotion
|
||||
</label>
|
||||
<select
|
||||
value={filters.emotion_id || ''}
|
||||
onChange={(e) => setFilters({ ...filters, emotion_id: e.target.value ? Number(e.target.value) : undefined })}
|
||||
className="w-full rounded-xl border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-candlelight-500"
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-select-group">
|
||||
<label className="fr-label" htmlFor="filtre-emotion">
|
||||
Émotion
|
||||
</label>
|
||||
<select
|
||||
id="filtre-emotion"
|
||||
className="fr-select"
|
||||
value={filters.emotion_id || ''}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
emotion_id: e.target.value ? Number(e.target.value) : undefined,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Toutes</option>
|
||||
{emotions?.map((emotion) => (
|
||||
<option key={emotion.id} value={emotion.id}>
|
||||
{emotion.icone} {emotion.nom}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md fr-mt-2w">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--tertiary fr-btn--sm"
|
||||
onClick={() => setFilters({})}
|
||||
>
|
||||
<option value="">Toutes</option>
|
||||
{emotions?.map((emotion) => (
|
||||
<option key={emotion.id} value={emotion.id}>
|
||||
{emotion.icone} {emotion.nom}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setFilters({})}
|
||||
>
|
||||
Réinitialiser
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timeline */}
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4 p-4 bg-white dark:bg-gray-900 rounded-xl">
|
||||
<Skeleton variant="circle" className="w-12 h-12" />
|
||||
<div className="flex-1">
|
||||
<Skeleton className="h-4 w-32 mb-2" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p>Chargement…</p>
|
||||
) : (
|
||||
<TrackerTimeline
|
||||
saisies={saisies || []}
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useRapport } from '@/hooks/useRapport';
|
||||
import EmotionChart from '@/components/features/EmotionChart';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Skeleton from '@/components/ui/Skeleton';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import { BarChart3, TrendingUp, PieChart } from 'lucide-react';
|
||||
|
||||
type Period = 'week' | 'month' | 'quarter' | 'year';
|
||||
|
||||
@@ -22,169 +18,192 @@ export default function RapportsPage() {
|
||||
const { data: rapport, isLoading } = useRapport(period);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
Mes rapports
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
<div className="fr-container fr-py-6w">
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-mb-4w">
|
||||
<div className="fr-col-12 fr-col-md">
|
||||
<h1 className="fr-mb-1v">Mes rapports</h1>
|
||||
<p className="fr-text--lead fr-mb-0">
|
||||
Analysez vos tendances émotionnelles
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Sélecteur de période */}
|
||||
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1">
|
||||
{PERIODS.map((p) => (
|
||||
<button
|
||||
key={p.value}
|
||||
onClick={() => setPeriod(p.value)}
|
||||
className={`
|
||||
px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200
|
||||
${period === p.value
|
||||
? 'bg-white dark:bg-gray-700 shadow-sm text-gray-900 dark:text-white'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="fr-col-12 fr-col-md--right">
|
||||
<fieldset className="fr-segmented fr-segmented--sm">
|
||||
<legend className="fr-segmented__legend">Période</legend>
|
||||
<div className="fr-segmented__elements">
|
||||
{PERIODS.map((p) => (
|
||||
<div className="fr-segmented__element" key={p.value}>
|
||||
<input
|
||||
type="radio"
|
||||
id={`period-${p.value}`}
|
||||
name="period"
|
||||
checked={period === p.value}
|
||||
onChange={() => setPeriod(p.value)}
|
||||
/>
|
||||
<label className="fr-label" htmlFor={`period-${p.value}`}>
|
||||
{p.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-6">
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<Skeleton className="h-4 w-24 mb-2" />
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<Skeleton variant="rect" className="h-80" />
|
||||
</Card>
|
||||
</div>
|
||||
<p>Chargement…</p>
|
||||
) : rapport ? (
|
||||
<div className="space-y-6">
|
||||
<>
|
||||
{/* Stats résumées */}
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<BarChart3 size={18} className="text-candlelight-500" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Total saisies</p>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{rapport.stats.total_saisies}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au {new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<TrendingUp size={18} className="text-malachite-500" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Intensité moyenne</p>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
|
||||
</p>
|
||||
<span className="text-sm text-gray-400">/10</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<PieChart size={18} className="text-blue-500" />
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Émotion dominante</p>
|
||||
</div>
|
||||
{rapport.stats.emotion_dominante ? (
|
||||
<div>
|
||||
<Badge color={rapport.stats.emotion_dominante.couleur}>
|
||||
{rapport.stats.emotion_dominante.nom}
|
||||
</Badge>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{rapport.stats.emotion_dominante.count} fois
|
||||
</p>
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-mb-3w">
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--vertical">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<span
|
||||
className="fr-icon-bar-chart-box-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Total saisies
|
||||
</h3>
|
||||
<p className="fr-h3 fr-mb-1v">{rapport.stats.total_saisies}</p>
|
||||
<p className="fr-tile__detail">
|
||||
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au{' '}
|
||||
{new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xl text-gray-300 dark:text-gray-600">—</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--vertical">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<span
|
||||
className="fr-icon-line-chart-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Intensité moyenne
|
||||
</h3>
|
||||
<p className="fr-h3 fr-mb-0">
|
||||
{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
|
||||
<span className="fr-text--sm"> /10</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--vertical">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<span
|
||||
className="fr-icon-pie-chart-2-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Émotion dominante
|
||||
</h3>
|
||||
{rapport.stats.emotion_dominante ? (
|
||||
<p className="fr-mb-0">
|
||||
<span className="fr-badge fr-badge--info">
|
||||
{rapport.stats.emotion_dominante.nom}
|
||||
</span>
|
||||
<span className="fr-text--sm fr-display-block fr-mt-1v">
|
||||
{rapport.stats.emotion_dominante.count} fois
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="fr-h4 fr-mb-0">—</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Répartition (Camembert) */}
|
||||
<Card>
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Répartition des émotions
|
||||
</h2>
|
||||
<div className="fr-card fr-p-3w fr-mb-3w">
|
||||
<h2 className="fr-h5">Répartition des émotions</h2>
|
||||
<EmotionChart data={rapport} type="repartition" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Évolution (Lignes) */}
|
||||
<Card>
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Évolution dans le temps
|
||||
</h2>
|
||||
<div className="fr-card fr-p-3w fr-mb-3w">
|
||||
<h2 className="fr-h5">Évolution dans le temps</h2>
|
||||
<EmotionChart data={rapport} type="evolution" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Intensité moyenne (Barres) */}
|
||||
<Card>
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Intensité moyenne par période
|
||||
</h2>
|
||||
<div className="fr-card fr-p-3w fr-mb-3w">
|
||||
<h2 className="fr-h5">Intensité moyenne par période</h2>
|
||||
<EmotionChart data={rapport} type="intensite" />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Détail répartition */}
|
||||
{rapport.repartition.length > 0 && (
|
||||
<Card>
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Détail par émotion
|
||||
</h2>
|
||||
<div className="space-y-3">
|
||||
<div className="fr-card fr-p-3w">
|
||||
<h2 className="fr-h5">Détail par émotion</h2>
|
||||
<ul className="fr-raw-list">
|
||||
{rapport.repartition.map((item) => (
|
||||
<div key={item.nom} className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full shrink-0"
|
||||
style={{ backgroundColor: item.couleur }}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 w-32">
|
||||
{item.nom}
|
||||
</span>
|
||||
<div className="flex-1 h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
<li key={item.nom} className="fr-py-1w">
|
||||
<div className="fr-grid-row fr-grid-row--middle">
|
||||
<span
|
||||
className="fr-mr-2w"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '0.75rem',
|
||||
height: '0.75rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: item.couleur,
|
||||
}}
|
||||
/>
|
||||
<span className="fr-col">
|
||||
<strong>{item.nom}</strong>
|
||||
</span>
|
||||
<span className="fr-text--sm fr-mr-3w">
|
||||
{item.pourcentage}%
|
||||
</span>
|
||||
<span className="fr-text--sm fr-text-mention--grey">
|
||||
Moy : {item.intensite_moyenne.toFixed(1)}/10
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="fr-mt-1v"
|
||||
style={{
|
||||
height: '0.5rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'var(--background-contrast-grey)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${item.pourcentage}%`,
|
||||
backgroundColor: item.couleur,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400 w-16 text-right">
|
||||
{item.pourcentage}%
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 w-20 text-right">
|
||||
Moy: {item.intensite_moyenne.toFixed(1)}/10
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<Card className="text-center py-16">
|
||||
<BarChart3 size={48} className="mx-auto mb-4 text-gray-300 dark:text-gray-600" />
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400">
|
||||
Pas de données pour cette période
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__title">Pas de données pour cette période</p>
|
||||
<p className="fr-callout__text">
|
||||
Enregistrez des saisies pour visualiser vos tendances.
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user