Merge branch 'refonte-dsfr'
This commit is contained in:
@@ -40,3 +40,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Assets DSFR copiés par le postinstall (régénérés depuis node_modules)
|
||||
/public/dsfr
|
||||
|
||||
@@ -12,6 +12,8 @@ const eslintConfig = defineConfig([
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
// Assets DSFR vendorisés (minifiés) — à ne pas linter.
|
||||
"public/**",
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
Generated
+39
@@ -8,6 +8,7 @@
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@codegouvfr/react-dsfr": "^1.32.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"axios": "^1.13.6",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -370,6 +371,29 @@
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@codegouvfr/react-dsfr": {
|
||||
"version": "1.32.4",
|
||||
"resolved": "https://registry.npmjs.org/@codegouvfr/react-dsfr/-/react-dsfr-1.32.4.tgz",
|
||||
"integrity": "sha512-yhjPqPObB3BsehZTRhJZ9qXwBRT2RZl0cn6hG838MlkPntTE5H5w0vU05KmKcdDBnQ0wDV59G7nnZrpKZQlNuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tsafe": "^1.8.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"copy-dsfr-to-public": "bin/copy-dsfr-to-public.js",
|
||||
"only-include-used-icons": "bin/only-include-used-icons.js",
|
||||
"react-dsfr": "bin/react-dsfr.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@gouvfr/dsfr-chart": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@gouvfr/dsfr-chart": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
@@ -8001,6 +8025,12 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tsafe": {
|
||||
"version": "1.8.12",
|
||||
"resolved": "https://registry.npmjs.org/tsafe/-/tsafe-1.8.12.tgz",
|
||||
"integrity": "sha512-nFRqW0ttu/2o6XTXsHiVZWJBCOaxhVqZLg7dgs3coZNsCMPXPfwz+zPHAQA+70fNnVJLAPg1EgGIqK9Q84tvAw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
@@ -8964,6 +8994,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "vitest run"
|
||||
"test": "vitest run",
|
||||
"postinstall": "node scripts/copy-dsfr.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codegouvfr/react-dsfr": "^1.32.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"axios": "^1.13.6",
|
||||
"lucide-react": "^0.577.0",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copie les assets du Système de Design de l'État (DSFR) vers public/dsfr,
|
||||
// pour les servir en statique (CSS, JS, polices Marianne, icônes).
|
||||
// Lancé automatiquement après `npm install` (script postinstall).
|
||||
import { cpSync, rmSync, existsSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const src = join(root, "node_modules", "@codegouvfr", "react-dsfr", "dsfr");
|
||||
const dest = join(root, "public", "dsfr");
|
||||
|
||||
if (!existsSync(src)) {
|
||||
console.warn("[copy-dsfr] Source DSFR introuvable, étape ignorée.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
rmSync(dest, { recursive: true, force: true });
|
||||
cpSync(src, dest, { recursive: true });
|
||||
console.log("[copy-dsfr] Assets DSFR copiés vers public/dsfr.");
|
||||
@@ -1,13 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Plus, Edit2, Trash2 } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
import type { Feed } from '@/types';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -90,92 +87,107 @@ export default function AdminContenusPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
Contenus
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-grid-row--gutters fr-mb-4w">
|
||||
<div className="fr-col">
|
||||
<h1 className="fr-mb-1v">Contenus</h1>
|
||||
<p className="fr-text--sm fr-mb-0">
|
||||
Gérez les articles et ressources de la plateforme
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={openCreate} size="sm">
|
||||
<Plus size={16} />
|
||||
Nouvel article
|
||||
</Button>
|
||||
<div className="fr-col-auto">
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--icon-left fr-icon-add-line"
|
||||
onClick={openCreate}
|
||||
>
|
||||
Nouvel article
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<div className="h-6 bg-gray-100 dark:bg-gray-800 rounded animate-pulse w-48 mb-2" />
|
||||
<div className="h-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse w-full" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<p>Chargement…</p>
|
||||
) : feeds.length === 0 ? (
|
||||
<Card className="text-center py-12">
|
||||
<p className="text-gray-400 dark:text-gray-600 mb-4">Aucun article</p>
|
||||
<Button onClick={openCreate} size="sm">Créer le premier article</Button>
|
||||
</Card>
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__text">Aucun article pour le moment.</p>
|
||||
<button type="button" className="fr-btn fr-btn--icon-left fr-icon-add-line" onClick={openCreate}>
|
||||
Créer le premier article
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{feeds.map((feed) => (
|
||||
<Card key={feed.id} className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white truncate">
|
||||
{feed.titre}
|
||||
</h3>
|
||||
<Badge color={feed.est_publie ? '#06c656' : '#9ca3af'}>
|
||||
{feed.est_publie ? 'Publié' : 'Brouillon'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
/{feed.slug} · Ordre: {feed.ordre}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => openEdit(feed)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(feed.id)}
|
||||
className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={16} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
<div className="fr-table fr-table--bordered">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Titre</th>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col">Ordre</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{feeds.map((feed) => (
|
||||
<tr key={feed.id}>
|
||||
<td>{feed.titre}</td>
|
||||
<td>/{feed.slug}</td>
|
||||
<td>{feed.ordre}</td>
|
||||
<td>
|
||||
<span className={`fr-badge fr-badge--sm ${feed.est_publie ? 'fr-badge--success' : 'fr-badge--info'}`}>
|
||||
{feed.est_publie ? 'Publié' : 'Brouillon'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openEdit(feed)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-edit-line"
|
||||
title="Modifier"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(feed.id)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
|
||||
title="Supprimer"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal création/édition */}
|
||||
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editingFeed ? 'Modifier l\'article' : 'Nouvel article'} size="lg">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit} className="fr-mt-2w">
|
||||
<Input
|
||||
label="Titre"
|
||||
value={form.titre}
|
||||
onChange={(e) => setForm({ ...form, titre: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="feed-contenu">
|
||||
Contenu (HTML)
|
||||
</label>
|
||||
<textarea
|
||||
id="feed-contenu"
|
||||
className="fr-input"
|
||||
value={form.contenu}
|
||||
onChange={(e) => setForm({ ...form, contenu: e.target.value })}
|
||||
rows={10}
|
||||
required
|
||||
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 font-mono text-sm resize-none"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
@@ -183,40 +195,47 @@ export default function AdminContenusPage() {
|
||||
value={form.image_url}
|
||||
onChange={(e) => setForm({ ...form, image_url: e.target.value })}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Ordre d'affichage
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={form.ordre}
|
||||
onChange={(e) => setForm({ ...form, ordre: parseInt(e.target.value) || 0 })}
|
||||
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-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-6">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="feed-ordre">
|
||||
Ordre d'affichage
|
||||
</label>
|
||||
<input
|
||||
id="feed-ordre"
|
||||
type="number"
|
||||
className="fr-input"
|
||||
value={form.ordre}
|
||||
onChange={(e) => setForm({ ...form, ordre: parseInt(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end pb-1">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="fr-col-6">
|
||||
<div className="fr-checkbox-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="feed-publie"
|
||||
checked={form.est_publie}
|
||||
onChange={(e) => setForm({ ...form, est_publie: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-gray-300 text-malachite-500 focus:ring-malachite-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="fr-label" htmlFor="feed-publie">
|
||||
Publié
|
||||
</span>
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button type="submit" loading={formLoading}>
|
||||
{editingFeed ? 'Mettre à jour' : 'Créer'}
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-mt-2w">
|
||||
<li>
|
||||
<Button type="submit" loading={formLoading}>
|
||||
{editingFeed ? 'Mettre à jour' : 'Créer'}
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="ghost" onClick={() => setModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Users, FileText, Heart } from 'lucide-react';
|
||||
import Card from '@/components/ui/Card';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface AdminStats {
|
||||
@@ -41,86 +39,63 @@ export default function AdminDashboardPage() {
|
||||
}, []);
|
||||
|
||||
const statCards = [
|
||||
{
|
||||
label: 'Utilisateurs',
|
||||
value: stats?.total_utilisateurs || 0,
|
||||
icon: <Users size={24} />,
|
||||
color: 'text-blue-500',
|
||||
bg: 'bg-blue-50 dark:bg-blue-950/30',
|
||||
},
|
||||
{
|
||||
label: 'Articles',
|
||||
value: stats?.total_feeds || 0,
|
||||
icon: <FileText size={24} />,
|
||||
color: 'text-malachite-500',
|
||||
bg: 'bg-malachite-50 dark:bg-malachite-950/30',
|
||||
},
|
||||
{
|
||||
label: 'Émotions',
|
||||
value: stats?.total_emotions || 0,
|
||||
icon: <Heart size={24} />,
|
||||
color: 'text-candlelight-500',
|
||||
bg: 'bg-candlelight-50 dark:bg-candlelight-950/30',
|
||||
},
|
||||
{ label: 'Utilisateurs', value: stats?.total_utilisateurs || 0, icon: 'fr-icon-user-line' },
|
||||
{ label: 'Articles', value: stats?.total_feeds || 0, icon: 'fr-icon-article-line' },
|
||||
{ label: 'Émotions', value: stats?.total_emotions || 0, icon: 'fr-icon-heart-line' },
|
||||
];
|
||||
|
||||
const quickActions = [
|
||||
{ href: '/admin/utilisateurs', icon: 'fr-icon-user-line', label: 'Gérer les utilisateurs' },
|
||||
{ href: '/admin/contenus', icon: 'fr-icon-article-line', label: 'Gérer les contenus' },
|
||||
{ href: '/admin/emotions', icon: 'fr-icon-heart-line', label: 'Gérer les émotions' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Administration
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Vue d'ensemble de la plateforme CESIZen
|
||||
</p>
|
||||
</div>
|
||||
<h1>Administration</h1>
|
||||
<p className="fr-text--lead fr-mb-4w">
|
||||
Vue d'ensemble de la plateforme CESIZen
|
||||
</p>
|
||||
|
||||
<div className="grid sm:grid-cols-3 gap-6 mb-8">
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-mb-4w">
|
||||
{statCards.map((card) => (
|
||||
<Card key={card.label}>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl ${card.bg} flex items-center justify-center ${card.color}`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{card.label}</p>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{loading ? '...' : card.value}
|
||||
</p>
|
||||
<div className="fr-col-12 fr-col-md-4" key={card.label}>
|
||||
<div className="fr-tile fr-tile--horizontal">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h2 className="fr-tile__title">
|
||||
<span className={`${card.icon} fr-mr-1w`} aria-hidden="true" />
|
||||
{card.label}
|
||||
</h2>
|
||||
<p className="fr-tile__detail fr-text--bold fr-display--xs">
|
||||
{loading ? '…' : card.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Actions rapides
|
||||
</h2>
|
||||
<div className="grid sm:grid-cols-3 gap-4">
|
||||
<Link
|
||||
href="/admin/utilisateurs"
|
||||
className="flex items-center gap-3 p-4 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<Users size={20} className="text-blue-500" />
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Gérer les utilisateurs</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/contenus"
|
||||
className="flex items-center gap-3 p-4 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<FileText size={20} className="text-malachite-500" />
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Gérer les contenus</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/emotions"
|
||||
className="flex items-center gap-3 p-4 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<Heart size={20} className="text-candlelight-500" />
|
||||
<span className="font-medium text-gray-700 dark:text-gray-300">Gérer les émotions</span>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
<h2>Actions rapides</h2>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{quickActions.map((action) => (
|
||||
<div className="fr-col-12 fr-col-md-4" key={action.href}>
|
||||
<div className="fr-tile fr-tile--horizontal fr-enlarge-link">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<Link href={action.href}>
|
||||
<span className={`${action.icon} fr-mr-1w`} aria-hidden="true" />
|
||||
{action.label}
|
||||
</Link>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Plus, Edit2, Trash2 } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
import type { Emotion } from '@/types';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -100,193 +97,268 @@ export default function AdminEmotionsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const colorSwatch = (couleur: string) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: couleur,
|
||||
verticalAlign: 'middle',
|
||||
marginRight: '0.5rem',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
Émotions
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Gérez le catalogue des émotions disponibles
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-grid-row--gutters fr-mb-4w">
|
||||
<div className="fr-col">
|
||||
<h1 className="fr-mb-1v">Émotions</h1>
|
||||
<p className="fr-text--sm fr-mb-0">
|
||||
Gérez le catalogue des émotions disponibles (2 niveaux)
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => openCreate()} size="sm">
|
||||
<Plus size={16} />
|
||||
Nouvelle émotion
|
||||
</Button>
|
||||
<div className="fr-col-auto">
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--icon-left fr-icon-add-line"
|
||||
onClick={() => openCreate()}
|
||||
>
|
||||
Nouvelle émotion
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<div className="h-6 bg-gray-100 dark:bg-gray-800 rounded animate-pulse w-32" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<p>Chargement…</p>
|
||||
) : emotions.length === 0 ? (
|
||||
<Card className="text-center py-12">
|
||||
<p className="text-gray-400 dark:text-gray-600 mb-4">Aucune émotion</p>
|
||||
<Button onClick={() => openCreate()} size="sm">Créer la première émotion</Button>
|
||||
</Card>
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__text">Aucune émotion pour le moment.</p>
|
||||
<button type="button" className="fr-btn fr-btn--icon-left fr-icon-add-line" onClick={() => openCreate()}>
|
||||
Créer la première émotion
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{parentEmotions.map((emotion) => (
|
||||
<Card key={emotion.id}>
|
||||
<div className="flex items-center gap-4 mb-3">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl shrink-0"
|
||||
style={{ backgroundColor: `${emotion.couleur}20` }}
|
||||
>
|
||||
{emotion.icone || '🔵'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||
{emotion.nom}
|
||||
</h3>
|
||||
<Badge color={emotion.couleur}>{emotion.couleur}</Badge>
|
||||
{!emotion.est_actif && (
|
||||
<Badge color="#9ca3af">Inactif</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">Niveau 1</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => openCreate(emotion.id)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="Ajouter sous-émotion"
|
||||
>
|
||||
<Plus size={16} className="text-malachite-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openEdit(emotion)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
title="Modifier"
|
||||
>
|
||||
<Edit2 size={16} className="text-gray-500" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(emotion.id)}
|
||||
className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={16} className="text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sous-émotions */}
|
||||
{emotion.enfants && emotion.enfants.length > 0 && (
|
||||
<div className="ml-8 pl-4 border-l-2 border-gray-200 dark:border-gray-700 space-y-2">
|
||||
{emotion.enfants.map((child) => (
|
||||
<div key={child.id} className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
||||
<span className="text-lg">{child.icone || '🔵'}</span>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{child.nom}
|
||||
</span>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full shrink-0"
|
||||
style={{ backgroundColor: child.couleur }}
|
||||
/>
|
||||
{!child.est_actif && <Badge color="#9ca3af">Inactif</Badge>}
|
||||
<button
|
||||
onClick={() => openEdit(child)}
|
||||
className="p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<Edit2 size={14} className="text-gray-400" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(child.id)}
|
||||
className="p-1.5 rounded hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} className="text-red-400" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
<div className="fr-table fr-table--bordered">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Émotion</th>
|
||||
<th scope="col">Niveau</th>
|
||||
<th scope="col">Couleur</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parentEmotions.map((emotion) => (
|
||||
<FragmentRows
|
||||
key={emotion.id}
|
||||
emotion={emotion}
|
||||
colorSwatch={colorSwatch}
|
||||
onAddChild={openCreate}
|
||||
onEdit={openEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal création/édition */}
|
||||
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title={editingEmotion ? 'Modifier l\'émotion' : 'Nouvelle émotion'}>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit} className="fr-mt-2w">
|
||||
<Input
|
||||
label="Nom"
|
||||
value={form.nom}
|
||||
onChange={(e) => setForm({ ...form, nom: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Couleur
|
||||
</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={form.couleur}
|
||||
onChange={(e) => setForm({ ...form, couleur: e.target.value })}
|
||||
className="w-10 h-10 rounded-lg border border-gray-300 dark:border-gray-700 cursor-pointer"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={form.couleur}
|
||||
onChange={(e) => setForm({ ...form, couleur: e.target.value })}
|
||||
className="flex-1 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 font-mono text-sm"
|
||||
/>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-6">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="emotion-couleur">Couleur</label>
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-grid-row--bottom">
|
||||
<div className="fr-col-auto">
|
||||
<input
|
||||
id="emotion-couleur"
|
||||
type="color"
|
||||
value={form.couleur}
|
||||
onChange={(e) => setForm({ ...form, couleur: e.target.value })}
|
||||
style={{ width: '2.5rem', height: '2.5rem', border: 'none', padding: 0, cursor: 'pointer' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="fr-col">
|
||||
<input
|
||||
type="text"
|
||||
className="fr-input"
|
||||
value={form.couleur}
|
||||
onChange={(e) => setForm({ ...form, couleur: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
label="Icône (emoji)"
|
||||
value={form.icone}
|
||||
onChange={(e) => setForm({ ...form, icone: e.target.value })}
|
||||
placeholder="😊"
|
||||
/>
|
||||
<div className="fr-col-6">
|
||||
<Input
|
||||
label="Icône (emoji)"
|
||||
value={form.icone}
|
||||
onChange={(e) => setForm({ ...form, icone: e.target.value })}
|
||||
placeholder="😊"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{form.niveau === 2 && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Émotion parente
|
||||
</label>
|
||||
<div className="fr-select-group">
|
||||
<label className="fr-label" htmlFor="emotion-parent">Émotion parente</label>
|
||||
<select
|
||||
id="emotion-parent"
|
||||
className="fr-select"
|
||||
value={form.parent_id || ''}
|
||||
onChange={(e) => setForm({ ...form, parent_id: Number(e.target.value) || null })}
|
||||
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"
|
||||
>
|
||||
<option value="">Sélectionner...</option>
|
||||
<option value="">Sélectionner…</option>
|
||||
{parentEmotions.map((e) => (
|
||||
<option key={e.id} value={e.id}>{e.icone} {e.nom}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="fr-checkbox-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="emotion-actif"
|
||||
checked={form.est_actif}
|
||||
onChange={(e) => setForm({ ...form, est_actif: e.target.checked })}
|
||||
className="w-4 h-4 rounded border-gray-300 text-malachite-500 focus:ring-malachite-500"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<label className="fr-label" htmlFor="emotion-actif">
|
||||
Active
|
||||
</span>
|
||||
</label>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button type="submit" loading={formLoading}>
|
||||
{editingEmotion ? 'Mettre à jour' : 'Créer'}
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-mt-2w">
|
||||
<li>
|
||||
<Button type="submit" loading={formLoading}>
|
||||
{editingEmotion ? 'Mettre à jour' : 'Créer'}
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="ghost" onClick={() => setModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FragmentRows({
|
||||
emotion,
|
||||
colorSwatch,
|
||||
onAddChild,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
emotion: Emotion;
|
||||
colorSwatch: (couleur: string) => React.ReactNode;
|
||||
onAddChild: (parentId?: number) => void;
|
||||
onEdit: (emotion: Emotion) => void;
|
||||
onDelete: (id: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>
|
||||
<span className="fr-mr-1w" aria-hidden="true">{emotion.icone || '🔵'}</span>
|
||||
<strong>{emotion.nom}</strong>
|
||||
</td>
|
||||
<td><span className="fr-badge fr-badge--sm">Niveau 1</span></td>
|
||||
<td>{colorSwatch(emotion.couleur)}<span className="fr-text--xs">{emotion.couleur}</span></td>
|
||||
<td>
|
||||
<span className={`fr-badge fr-badge--sm ${emotion.est_actif ? 'fr-badge--success' : 'fr-badge--error'}`}>
|
||||
{emotion.est_actif ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onAddChild(emotion.id)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-add-line"
|
||||
title="Ajouter une sous-émotion"
|
||||
>
|
||||
Sous-émotion
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit(emotion)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-edit-line"
|
||||
title="Modifier"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(emotion.id)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
|
||||
title="Supprimer"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{emotion.enfants?.map((child) => (
|
||||
<tr key={child.id}>
|
||||
<td>
|
||||
<span className="fr-icon-arrow-right-line fr-icon--sm fr-mr-1w" aria-hidden="true" />
|
||||
<span className="fr-mr-1w" aria-hidden="true">{child.icone || '🔵'}</span>
|
||||
{child.nom}
|
||||
</td>
|
||||
<td><span className="fr-badge fr-badge--sm fr-badge--info">Niveau 2</span></td>
|
||||
<td>{colorSwatch(child.couleur)}<span className="fr-text--xs">{child.couleur}</span></td>
|
||||
<td>
|
||||
<span className={`fr-badge fr-badge--sm ${child.est_actif ? 'fr-badge--success' : 'fr-badge--error'}`}>
|
||||
{child.est_actif ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit(child)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-edit-line"
|
||||
title="Modifier"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(child.id)}
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
|
||||
title="Supprimer"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
// /admin n'a pas d'écran propre : on redirige vers le tableau de bord admin.
|
||||
export default function AdminIndexPage() {
|
||||
redirect("/admin/dashboard");
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Search, UserCheck, UserX, Trash2, Plus } from 'lucide-react';
|
||||
import api from '@/lib/api';
|
||||
import type { Utilisateur } from '@/types';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -76,145 +73,153 @@ export default function AdminUtilisateursPage() {
|
||||
`${u.prenom} ${u.nom} ${u.email}`.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
const roleColor = (nom: string) => {
|
||||
const roleBadgeClass = (nom?: string) => {
|
||||
switch (nom) {
|
||||
case 'administrateur': return '#ef4444';
|
||||
case 'membre': return '#06c656';
|
||||
default: return '#9ca3af';
|
||||
case 'administrateur': return 'fr-badge fr-badge--error';
|
||||
case 'membre': return 'fr-badge fr-badge--success';
|
||||
default: return 'fr-badge';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||
Utilisateurs
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-grid-row--gutters fr-mb-4w">
|
||||
<div className="fr-col">
|
||||
<h1 className="fr-mb-1v">Utilisateurs</h1>
|
||||
<p className="fr-text--sm fr-mb-0">
|
||||
{utilisateurs.length} utilisateur{utilisateurs.length > 1 ? 's' : ''} enregistré{utilisateurs.length > 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => setCreateModalOpen(true)} size="sm">
|
||||
<Plus size={16} />
|
||||
Nouvel utilisateur
|
||||
</Button>
|
||||
<div className="fr-col-auto">
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--icon-left fr-icon-add-line"
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
Nouvel utilisateur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recherche */}
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
placeholder="Rechercher un utilisateur..."
|
||||
icon={<Search size={18} />}
|
||||
<div className="fr-search-bar fr-mb-4w" role="search">
|
||||
<label className="fr-label" htmlFor="recherche-utilisateur">
|
||||
Rechercher un utilisateur
|
||||
</label>
|
||||
<input
|
||||
className="fr-input"
|
||||
id="recherche-utilisateur"
|
||||
type="search"
|
||||
placeholder="Rechercher un utilisateur…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
<button type="button" className="fr-btn" title="Rechercher">
|
||||
Rechercher
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<Card className="overflow-hidden p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Utilisateur</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Email</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Rôle</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Statut</th>
|
||||
<th className="text-right px-6 py-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">Actions</th>
|
||||
<div className="fr-table fr-table--bordered">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Utilisateur</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Rôle</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={5}>Chargement…</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{loading ? (
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<tr key={i}>
|
||||
<td colSpan={5} className="px-6 py-4">
|
||||
<div className="h-4 bg-gray-100 dark:bg-gray-800 rounded animate-pulse" />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : filtered.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-8 text-center text-gray-400">
|
||||
Aucun utilisateur trouvé
|
||||
) : filtered.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5}>Aucun utilisateur trouvé</td>
|
||||
</tr>
|
||||
) : (
|
||||
filtered.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td>{user.prenom} {user.nom}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<span className={roleBadgeClass(user.role?.nom)}>
|
||||
{user.role?.nom || 'N/A'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filtered.map((user) => (
|
||||
<tr key={user.id} className="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<span className="font-medium text-gray-900 dark:text-white">
|
||||
{user.prenom} {user.nom}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{user.email}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge color={roleColor(user.role?.nom)}>
|
||||
{user.role?.nom || 'N/A'}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center gap-1.5 text-sm ${user.est_actif ? 'text-malachite-600' : 'text-red-500'}`}>
|
||||
<span className={`w-2 h-2 rounded-full ${user.est_actif ? 'bg-malachite-500' : 'bg-red-500'}`} />
|
||||
{user.est_actif ? 'Actif' : 'Inactif'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<td>
|
||||
<span className={`fr-badge fr-badge--sm ${user.est_actif ? 'fr-badge--success' : 'fr-badge--error'}`}>
|
||||
{user.est_actif ? 'Actif' : 'Inactif'}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToggleActive(user.id)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
className={`fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left ${user.est_actif ? 'fr-icon-close-circle-line' : 'fr-icon-checkbox-circle-line'}`}
|
||||
title={user.est_actif ? 'Désactiver' : 'Activer'}
|
||||
>
|
||||
{user.est_actif
|
||||
? <UserX size={16} className="text-red-500" />
|
||||
: <UserCheck size={16} className="text-malachite-500" />
|
||||
}
|
||||
{user.est_actif ? 'Désactiver' : 'Activer'}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-950/30 transition-colors"
|
||||
className="fr-btn fr-btn--sm fr-btn--tertiary fr-btn--icon-left fr-icon-delete-line"
|
||||
title="Supprimer"
|
||||
>
|
||||
<Trash2 size={16} className="text-red-500" />
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Modal création */}
|
||||
<Modal isOpen={createModalOpen} onClose={() => setCreateModalOpen(false)} title="Nouvel utilisateur">
|
||||
<form onSubmit={handleCreate} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="Prénom" value={newUser.prenom} onChange={(e) => setNewUser({ ...newUser, prenom: e.target.value })} required />
|
||||
<Input label="Nom" value={newUser.nom} onChange={(e) => setNewUser({ ...newUser, nom: e.target.value })} required />
|
||||
<form onSubmit={handleCreate} className="fr-mt-2w">
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-6">
|
||||
<Input label="Prénom" value={newUser.prenom} onChange={(e) => setNewUser({ ...newUser, prenom: e.target.value })} required />
|
||||
</div>
|
||||
<div className="fr-col-6">
|
||||
<Input label="Nom" value={newUser.nom} onChange={(e) => setNewUser({ ...newUser, nom: e.target.value })} required />
|
||||
</div>
|
||||
</div>
|
||||
<Input label="Email" type="email" value={newUser.email} onChange={(e) => setNewUser({ ...newUser, email: e.target.value })} required />
|
||||
<Input label="Mot de passe" type="password" value={newUser.password} onChange={(e) => setNewUser({ ...newUser, password: e.target.value })} required />
|
||||
<Input label="Confirmer" type="password" value={newUser.password_confirmation} onChange={(e) => setNewUser({ ...newUser, password_confirmation: e.target.value })} required />
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Rôle</label>
|
||||
<div className="fr-select-group">
|
||||
<label className="fr-label" htmlFor="role-select">Rôle</label>
|
||||
<select
|
||||
id="role-select"
|
||||
className="fr-select"
|
||||
value={newUser.role_id}
|
||||
onChange={(e) => setNewUser({ ...newUser, role_id: Number(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"
|
||||
>
|
||||
<option value={2}>Membre</option>
|
||||
<option value={3}>Administrateur</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button type="submit" loading={formLoading}>Créer</Button>
|
||||
<Button variant="ghost" onClick={() => setCreateModalOpen(false)}>Annuler</Button>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-mt-2w">
|
||||
<li>
|
||||
<Button type="submit" loading={formLoading}>Créer</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="ghost" onClick={() => setCreateModalOpen(false)}>Annuler</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
@@ -7,16 +7,18 @@ export default function AdminLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<>
|
||||
<Header />
|
||||
<div className="flex flex-1">
|
||||
<Sidebar />
|
||||
<main className="flex-1 bg-gray-50 dark:bg-gray-950">
|
||||
<div className="p-6 lg:p-8">
|
||||
{children}
|
||||
<div className="fr-container--fluid fr-py-4w">
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-12 fr-col-md-3 fr-col-lg-2">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</main>
|
||||
<main className="fr-col-12 fr-col-md-9 fr-col-lg-10" id="contenu">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Eye, EyeOff, Mail, Lock } from 'lucide-react';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -14,10 +13,12 @@ export default function ConnexionPage() {
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
await login(email, password);
|
||||
@@ -26,6 +27,7 @@ export default function ConnexionPage() {
|
||||
} catch (error: unknown) {
|
||||
const axiosError = error as import('axios').AxiosError<{ message?: string }>;
|
||||
const message = axiosError.response?.data?.message || 'Identifiants incorrects';
|
||||
setErrorMessage(message);
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -33,91 +35,73 @@ export default function ConnexionPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-candlelight-50 via-white to-malachite-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 px-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block">
|
||||
<h1 className="font-display text-4xl font-bold">
|
||||
<span className="text-malachite-600 dark:text-malachite-400">CESI</span>
|
||||
<span className="text-candlelight-500">ZEN</span>
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-2">Connectez-vous à votre compte</p>
|
||||
<>
|
||||
<h1>Connexion</h1>
|
||||
<p className="fr-text--lead">Connectez-vous à votre compte CESIZen.</p>
|
||||
|
||||
{errorMessage && (
|
||||
<div className="fr-alert fr-alert--error fr-alert--sm fr-mb-3w" role="alert">
|
||||
<p>{errorMessage}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="email">
|
||||
Adresse email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="fr-input"
|
||||
placeholder="vous@exemple.fr"
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-800 p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Adresse email
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="vous@exemple.fr"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full pl-11 pr-12 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="Votre mot de passe"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-candlelight-500 hover:bg-candlelight-600 disabled:opacity-50 disabled:cursor-not-allowed text-black font-semibold py-3 rounded-xl transition-all duration-200 hover:shadow-lg"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Connexion...
|
||||
</span>
|
||||
) : (
|
||||
'Se connecter'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Pas encore de compte ?{' '}
|
||||
<Link href="/inscription" className="text-malachite-600 dark:text-malachite-400 font-medium hover:underline">
|
||||
Créer un compte
|
||||
</Link>
|
||||
<div className="fr-password" id="password-group">
|
||||
<label className="fr-label" htmlFor="password">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="fr-input-wrap">
|
||||
<input
|
||||
id="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="fr-password__input fr-input"
|
||||
placeholder="Votre mot de passe"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="fr-password__checkbox fr-checkbox-group fr-checkbox-group--sm">
|
||||
<input
|
||||
id="password-show"
|
||||
type="checkbox"
|
||||
checked={showPassword}
|
||||
onChange={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
<label className="fr-label" htmlFor="password-show">
|
||||
Afficher le mot de passe
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading} className="fr-btn fr-btn--lg">
|
||||
{loading ? 'Connexion…' : 'Se connecter'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="fr-mt-4w">
|
||||
Pas encore de compte ?{' '}
|
||||
<Link href="/inscription" className="fr-link">
|
||||
Créer un compte
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { User, Mail, Lock, Eye, EyeOff } from 'lucide-react';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -50,174 +49,178 @@ export default function InscriptionPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-linear-to-br from-candlelight-50 via-white to-malachite-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 px-4 py-12">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="text-center mb-8">
|
||||
<Link href="/" className="inline-block">
|
||||
<h1 className="font-display text-4xl font-bold">
|
||||
<span className="text-malachite-600 dark:text-malachite-400">CESI</span>
|
||||
<span className="text-candlelight-500">ZEN</span>
|
||||
</h1>
|
||||
</Link>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-2">Créez votre compte gratuitement</p>
|
||||
</div>
|
||||
<>
|
||||
<h1>Créer un compte</h1>
|
||||
<p className="fr-text--lead">Créez votre compte CESIZen gratuitement.</p>
|
||||
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-800 p-8">
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="prenom" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Prénom
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="prenom"
|
||||
name="prenom"
|
||||
type="text"
|
||||
value={form.prenom}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="Jean"
|
||||
/>
|
||||
</div>
|
||||
{errors.prenom && <p className="text-red-500 text-xs mt-1">{errors.prenom[0]}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nom" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Nom
|
||||
</label>
|
||||
<input
|
||||
id="nom"
|
||||
name="nom"
|
||||
type="text"
|
||||
value={form.nom}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="Dupont"
|
||||
/>
|
||||
{errors.nom && <p className="text-red-500 text-xs mt-1">{errors.nom[0]}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Adresse email
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-12 fr-col-md-6">
|
||||
<div className={`fr-input-group${errors.prenom ? ' fr-input-group--error' : ''}`}>
|
||||
<label className="fr-label" htmlFor="prenom">
|
||||
Prénom
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="vous@exemple.fr"
|
||||
/>
|
||||
</div>
|
||||
{errors.email && <p className="text-red-500 text-xs mt-1">{errors.email[0]}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
minLength={8}
|
||||
className="w-full pl-11 pr-12 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="Minimum 8 caractères"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
|
||||
>
|
||||
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
{errors.password && <p className="text-red-500 text-xs mt-1">{errors.password[0]}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password_confirmation" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||
<input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={form.password_confirmation}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full pl-11 pr-4 py-3 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-candlelight-500 focus:border-transparent outline-none transition-all"
|
||||
placeholder="Retapez votre mot de passe"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-3 pt-2">
|
||||
<input
|
||||
id="consentement_rgpd"
|
||||
name="consentement_rgpd"
|
||||
type="checkbox"
|
||||
checked={form.consentement_rgpd}
|
||||
id="prenom"
|
||||
name="prenom"
|
||||
type="text"
|
||||
value={form.prenom}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="mt-1 w-4 h-4 rounded border-gray-300 text-candlelight-500 focus:ring-candlelight-500"
|
||||
className="fr-input"
|
||||
placeholder="Jean"
|
||||
aria-describedby={errors.prenom ? 'prenom-error' : undefined}
|
||||
/>
|
||||
<label htmlFor="consentement_rgpd" className="text-sm text-gray-600 dark:text-gray-400">
|
||||
J'accepte le traitement de mes données personnelles conformément à la{' '}
|
||||
<Link href="/politique-de-confidentialite" className="text-malachite-600 dark:text-malachite-400 hover:underline">
|
||||
politique de confidentialité
|
||||
</Link>
|
||||
. Mes données sont chiffrées et je peux supprimer mon compte à tout moment.
|
||||
</label>
|
||||
</div>
|
||||
{errors.consentement_rgpd && <p className="text-red-500 text-xs">{errors.consentement_rgpd[0]}</p>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !form.consentement_rgpd}
|
||||
className="w-full bg-malachite-500 hover:bg-malachite-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-3 rounded-xl transition-all duration-200 hover:shadow-lg mt-2"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<svg className="animate-spin w-5 h-5" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
Inscription...
|
||||
</span>
|
||||
) : (
|
||||
'Créer mon compte'
|
||||
{errors.prenom && (
|
||||
<p id="prenom-error" className="fr-error-text">
|
||||
{errors.prenom[0]}
|
||||
</p>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
Déjà un compte ?{' '}
|
||||
<Link href="/connexion" className="text-malachite-600 dark:text-malachite-400 font-medium hover:underline">
|
||||
Se connecter
|
||||
</Link>
|
||||
<div className="fr-col-12 fr-col-md-6">
|
||||
<div className={`fr-input-group${errors.nom ? ' fr-input-group--error' : ''}`}>
|
||||
<label className="fr-label" htmlFor="nom">
|
||||
Nom
|
||||
</label>
|
||||
<input
|
||||
id="nom"
|
||||
name="nom"
|
||||
type="text"
|
||||
value={form.nom}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="fr-input"
|
||||
placeholder="Dupont"
|
||||
aria-describedby={errors.nom ? 'nom-error' : undefined}
|
||||
/>
|
||||
{errors.nom && (
|
||||
<p id="nom-error" className="fr-error-text">
|
||||
{errors.nom[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`fr-input-group${errors.email ? ' fr-input-group--error' : ''}`}>
|
||||
<label className="fr-label" htmlFor="email">
|
||||
Adresse email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="fr-input"
|
||||
placeholder="vous@exemple.fr"
|
||||
autoComplete="email"
|
||||
aria-describedby={errors.email ? 'email-error' : undefined}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p id="email-error" className="fr-error-text">
|
||||
{errors.email[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`fr-password${errors.password ? ' fr-input-group--error' : ''}`}>
|
||||
<label className="fr-label" htmlFor="password">
|
||||
Mot de passe
|
||||
</label>
|
||||
<div className="fr-input-wrap">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={form.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
minLength={8}
|
||||
className="fr-password__input fr-input"
|
||||
placeholder="Minimum 8 caractères"
|
||||
autoComplete="new-password"
|
||||
aria-describedby={errors.password ? 'password-error' : undefined}
|
||||
/>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p id="password-error" className="fr-error-text">
|
||||
{errors.password[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="password_confirmation">
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={form.password_confirmation}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="fr-input"
|
||||
placeholder="Retapez votre mot de passe"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="fr-checkbox-group fr-checkbox-group--sm">
|
||||
<input
|
||||
id="password-show"
|
||||
type="checkbox"
|
||||
checked={showPassword}
|
||||
onChange={() => setShowPassword(!showPassword)}
|
||||
/>
|
||||
<label className="fr-label" htmlFor="password-show">
|
||||
Afficher les mots de passe
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={`fr-checkbox-group fr-mt-3w${errors.consentement_rgpd ? ' fr-input-group--error' : ''}`}>
|
||||
<input
|
||||
id="consentement_rgpd"
|
||||
name="consentement_rgpd"
|
||||
type="checkbox"
|
||||
checked={form.consentement_rgpd}
|
||||
onChange={handleChange}
|
||||
required
|
||||
aria-describedby={errors.consentement_rgpd ? 'consentement-error' : undefined}
|
||||
/>
|
||||
<label className="fr-label" htmlFor="consentement_rgpd">
|
||||
J'accepte le traitement de mes données personnelles conformément à la{' '}
|
||||
<Link href="/politique-de-confidentialite" className="fr-link">
|
||||
politique de confidentialité
|
||||
</Link>
|
||||
. Mes données sont chiffrées et je peux supprimer mon compte à tout moment.
|
||||
</label>
|
||||
{errors.consentement_rgpd && (
|
||||
<p id="consentement-error" className="fr-error-text">
|
||||
{errors.consentement_rgpd[0]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !form.consentement_rgpd}
|
||||
className="fr-btn fr-btn--lg fr-mt-3w"
|
||||
>
|
||||
{loading ? 'Inscription…' : 'Créer mon compte'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="fr-mt-4w">
|
||||
Déjà un compte ?{' '}
|
||||
<Link href="/connexion" className="fr-link">
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="fr-container fr-py-8w">
|
||||
<div className="fr-grid-row fr-grid-row--center">
|
||||
<div className="fr-col-12 fr-col-md-8 fr-col-lg-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { Heart, BarChart3, Plus, BookOpen } from 'lucide-react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { useSaisies } from '@/hooks/useTracker';
|
||||
import { useRapport } from '@/hooks/useRapport';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Badge from '@/components/ui/Badge';
|
||||
import Skeleton from '@/components/ui/Skeleton';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAuth();
|
||||
@@ -17,177 +13,202 @@ export default function DashboardPage() {
|
||||
const recentSaisies = saisies?.slice(0, 5) || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="fr-container fr-py-6w">
|
||||
{/* En-tête */}
|
||||
<div className="mb-8">
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
Bonjour {user?.prenom} 👋
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Voici un aperçu de votre bien-être cette semaine.
|
||||
</p>
|
||||
</div>
|
||||
<h1>Bonjour {user?.prenom}</h1>
|
||||
<p className="fr-text--lead">
|
||||
Voici un aperçu de votre bien-être cette semaine.
|
||||
</p>
|
||||
|
||||
{/* Actions rapides */}
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
|
||||
<Link href="/journal/nouvelle-saisie">
|
||||
<Card hover className="flex items-center gap-4 bg-linear-to-r from-candlelight-50 to-candlelight-100 dark:from-candlelight-950/30 dark:to-candlelight-900/20 border-candlelight-200 dark:border-candlelight-800">
|
||||
<div className="w-12 h-12 rounded-xl bg-candlelight-200 dark:bg-candlelight-900 flex items-center justify-center">
|
||||
<Plus className="text-candlelight-700 dark:text-candlelight-400" size={24} />
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-mb-4w">
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--horizontal fr-enlarge-link">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<Link href="/journal/nouvelle-saisie">
|
||||
<span
|
||||
className="fr-icon-add-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Nouvelle saisie
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="fr-tile__detail">Enregistrer une émotion</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">Nouvelle saisie</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Enregistrer une émotion</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/journal">
|
||||
<Card hover className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-malachite-100 dark:bg-malachite-900 flex items-center justify-center">
|
||||
<BookOpen className="text-malachite-600 dark:text-malachite-400" size={24} />
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--horizontal fr-enlarge-link">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<Link href="/journal">
|
||||
<span
|
||||
className="fr-icon-book-2-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Mon journal
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="fr-tile__detail">Voir toutes mes saisies</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">Mon journal</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Voir toutes mes saisies</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="/journal/rapports">
|
||||
<Card hover className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900 flex items-center justify-center">
|
||||
<BarChart3 className="text-blue-600 dark:text-blue-400" size={24} />
|
||||
<div className="fr-col-12 fr-col-md-4">
|
||||
<div className="fr-tile fr-tile--horizontal fr-enlarge-link">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<Link href="/journal/rapports">
|
||||
<span
|
||||
className="fr-icon-line-chart-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Mes rapports
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="fr-tile__detail">Visualiser mes tendances</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white">Mes rapports</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Visualiser mes tendances</p>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats de la semaine */}
|
||||
<div className="grid sm:grid-cols-3 gap-4 mb-8">
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-mb-4w">
|
||||
{rapportLoading ? (
|
||||
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 className="fr-col-12 fr-col-md-4" key={i}>
|
||||
<div className="fr-tile fr-tile--vertical">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<p className="fr-tile__detail">Chargement…</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Card>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">Saisies cette semaine</p>
|
||||
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{rapport?.stats.total_saisies || 0}
|
||||
</p>
|
||||
</Card>
|
||||
<Card>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">Intensité moyenne</p>
|
||||
<div className="flex items-center 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>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">Émotion dominante</p>
|
||||
{rapport?.stats.emotion_dominante ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge color={rapport.stats.emotion_dominante.couleur}>
|
||||
{rapport.stats.emotion_dominante.nom}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-400">
|
||||
({rapport.stats.emotion_dominante.count}x)
|
||||
</span>
|
||||
<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">
|
||||
<p className="fr-tile__detail">Saisies cette semaine</p>
|
||||
<p className="fr-h2 fr-mb-0">
|
||||
{rapport?.stats.total_saisies || 0}
|
||||
</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">
|
||||
<p className="fr-tile__detail">Intensité moyenne</p>
|
||||
<p className="fr-h2 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">
|
||||
<p className="fr-tile__detail">Émotion dominante</p>
|
||||
{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">
|
||||
{' '}
|
||||
({rapport.stats.emotion_dominante.count}x)
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<p className="fr-h4 fr-mb-0">—</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Saisies récentes */}
|
||||
<Card>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Saisies récentes
|
||||
</h2>
|
||||
<Link
|
||||
href="/journal"
|
||||
className="text-sm text-malachite-600 dark:text-malachite-400 hover:underline"
|
||||
>
|
||||
Voir tout →
|
||||
</Link>
|
||||
</div>
|
||||
<div className="fr-card">
|
||||
<div className="fr-card__body">
|
||||
<div className="fr-card__content">
|
||||
<div className="fr-grid-row fr-grid-row--middle fr-mb-2w">
|
||||
<h2 className="fr-col fr-h5 fr-mb-0">Saisies récentes</h2>
|
||||
<p className="fr-col--right fr-mb-0">
|
||||
<Link href="/journal" className="fr-link">
|
||||
Voir tout
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{saisiesLoading ? (
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-3">
|
||||
<Skeleton variant="circle" />
|
||||
<div className="flex-1">
|
||||
<Skeleton className="h-4 w-32 mb-1" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : recentSaisies.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{recentSaisies.map((saisie) => (
|
||||
<div
|
||||
key={saisie.id}
|
||||
className="flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
|
||||
>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-lg shrink-0"
|
||||
style={{ backgroundColor: `${saisie.emotion?.couleur}20` }}
|
||||
{saisiesLoading ? (
|
||||
<p>Chargement…</p>
|
||||
) : recentSaisies.length > 0 ? (
|
||||
<ul className="fr-raw-list">
|
||||
{recentSaisies.map((saisie) => (
|
||||
<li
|
||||
key={saisie.id}
|
||||
className="fr-py-1w"
|
||||
style={{ borderBottom: '1px solid var(--border-default-grey)' }}
|
||||
>
|
||||
<div className="fr-grid-row fr-grid-row--middle">
|
||||
<span className="fr-mr-2w" style={{ fontSize: '1.5rem' }}>
|
||||
{saisie.emotion?.icone || '🔵'}
|
||||
</span>
|
||||
<span className="fr-col">
|
||||
<strong>{saisie.emotion?.nom}</strong>{' '}
|
||||
<span className="fr-text--sm">
|
||||
{saisie.intensite}/10
|
||||
</span>
|
||||
{saisie.note && (
|
||||
<span className="fr-text--sm fr-text-mention--grey fr-display-block">
|
||||
{saisie.note}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="fr-text--sm fr-text-mention--grey">
|
||||
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__text">Aucune saisie pour le moment.</p>
|
||||
<Link
|
||||
className="fr-btn fr-btn--sm fr-icon-add-line fr-btn--icon-left"
|
||||
href="/journal/nouvelle-saisie"
|
||||
>
|
||||
{saisie.emotion?.icone || '🔵'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900 dark:text-white text-sm">
|
||||
{saisie.emotion?.nom}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{saisie.intensite}/10
|
||||
</span>
|
||||
</div>
|
||||
{saisie.note && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||
{saisie.note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
|
||||
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
})}
|
||||
</span>
|
||||
Faire ma première saisie
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-400 dark:text-gray-600">
|
||||
<Heart size={32} className="mx-auto mb-2 opacity-50" />
|
||||
<p>Aucune saisie pour le moment</p>
|
||||
<Link
|
||||
href="/journal/nouvelle-saisie"
|
||||
className="text-sm text-candlelight-600 hover:underline mt-2 inline-block"
|
||||
>
|
||||
Faire ma première saisie
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,139 @@ 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="app-panel 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>
|
||||
{/* Navigation (flex simple pour éviter le rognage des libellés
|
||||
dans un groupe DSFR avec boutons à icône) */}
|
||||
<div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--secondary fr-btn--icon-left fr-icon-arrow-left-line"
|
||||
onClick={() => setStep(step - 1)}
|
||||
disabled={step === 1}
|
||||
>
|
||||
Précédent
|
||||
</button>
|
||||
{step < 3 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--icon-right fr-icon-arrow-right-line"
|
||||
onClick={() => setStep(step + 1)}
|
||||
disabled={!canNext()}
|
||||
>
|
||||
Suivant
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="fr-btn fr-btn--icon-left fr-icon-check-line"
|
||||
onClick={handleSubmit}
|
||||
disabled={!emotionId || createSaisie.isPending}
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
)}
|
||||
</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>
|
||||
)}
|
||||
</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="app-panel 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="app-panel 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="app-panel 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="app-panel 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="app-panel 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>
|
||||
);
|
||||
|
||||
@@ -7,12 +7,15 @@ export default function ProtectedLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
||||
<Header />
|
||||
<main className="flex-1 bg-gray-50 dark:bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{children}
|
||||
</div>
|
||||
<main
|
||||
role="main"
|
||||
id="contenu"
|
||||
className="fr-background-alt--grey"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<div className="fr-container fr-py-6w">{children}</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { User, Mail, Lock, Shield, Trash2 } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import api from '@/lib/api';
|
||||
import Modal from '@/components/ui/Modal';
|
||||
@@ -76,157 +73,229 @@ export default function ProfilPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="font-display text-3xl font-bold text-gray-900 dark:text-white mb-8">
|
||||
Mon profil
|
||||
</h1>
|
||||
<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">
|
||||
<h1>Mon profil</h1>
|
||||
|
||||
{/* Infos du profil */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-16 h-16 rounded-full bg-malachite-100 dark:bg-malachite-900 flex items-center justify-center">
|
||||
<User size={28} className="text-malachite-600 dark:text-malachite-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{/* Infos du profil */}
|
||||
<div className="app-panel fr-p-3w fr-mb-3w">
|
||||
<h2 className="fr-h4 fr-mb-1v">
|
||||
{user?.prenom} {user?.nom}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{user?.email}</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
Membre depuis le {user?.created_at ? new Date(user.created_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' }) : '—'}
|
||||
<p className="fr-text--sm fr-mb-1v">{user?.email}</p>
|
||||
<p className="fr-text--xs fr-text-mention--grey fr-mb-3w">
|
||||
Membre depuis le{' '}
|
||||
{user?.created_at
|
||||
? new Date(user.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})
|
||||
: '—'}
|
||||
</p>
|
||||
|
||||
{editing ? (
|
||||
<form onSubmit={handleUpdateProfil}>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
<div className="fr-col-12 fr-col-md-6">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="profil-prenom">
|
||||
Prénom
|
||||
</label>
|
||||
<input
|
||||
id="profil-prenom"
|
||||
className="fr-input"
|
||||
value={form.prenom}
|
||||
onChange={(e) => setForm({ ...form, prenom: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fr-col-12 fr-col-md-6">
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="profil-nom">
|
||||
Nom
|
||||
</label>
|
||||
<input
|
||||
id="profil-nom"
|
||||
className="fr-input"
|
||||
value={form.nom}
|
||||
onChange={(e) => setForm({ ...form, nom: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="profil-email">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="profil-email"
|
||||
className="fr-input"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md">
|
||||
<li>
|
||||
<Button type="submit" loading={loading}>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="secondary" type="button" onClick={() => setEditing(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setForm({
|
||||
nom: user?.nom || '',
|
||||
prenom: user?.prenom || '',
|
||||
email: user?.email || '',
|
||||
});
|
||||
setEditing(true);
|
||||
}}
|
||||
>
|
||||
Modifier mes informations
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{editing ? (
|
||||
<form onSubmit={handleUpdateProfil} className="space-y-4">
|
||||
<div className="grid sm:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Prénom"
|
||||
value={form.prenom}
|
||||
onChange={(e) => setForm({ ...form, prenom: e.target.value })}
|
||||
icon={<User size={18} />}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Nom"
|
||||
value={form.nom}
|
||||
onChange={(e) => setForm({ ...form, nom: e.target.value })}
|
||||
icon={<User size={18} />}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
icon={<Mail size={18} />}
|
||||
required
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" loading={loading}>
|
||||
Enregistrer
|
||||
{/* Changer mot de passe */}
|
||||
<div className="app-panel fr-p-3w fr-mb-3w">
|
||||
<h2 className="fr-h5">
|
||||
<span className="fr-icon-lock-line fr-mr-1w" aria-hidden="true" />
|
||||
Mot de passe
|
||||
</h2>
|
||||
|
||||
{changingPassword ? (
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="fr-password fr-input-group">
|
||||
<label className="fr-label" htmlFor="pwd-actuel">
|
||||
Mot de passe actuel
|
||||
</label>
|
||||
<input
|
||||
id="pwd-actuel"
|
||||
className="fr-input"
|
||||
type="password"
|
||||
value={passwordForm.ancien_mot_de_passe}
|
||||
onChange={(e) =>
|
||||
setPasswordForm({ ...passwordForm, ancien_mot_de_passe: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="fr-password fr-input-group">
|
||||
<label className="fr-label" htmlFor="pwd-nouveau">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="pwd-nouveau"
|
||||
className="fr-input"
|
||||
type="password"
|
||||
value={passwordForm.password}
|
||||
onChange={(e) =>
|
||||
setPasswordForm({ ...passwordForm, password: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="fr-password fr-input-group">
|
||||
<label className="fr-label" htmlFor="pwd-confirm">
|
||||
Confirmer le nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="pwd-confirm"
|
||||
className="fr-input"
|
||||
type="password"
|
||||
value={passwordForm.password_confirmation}
|
||||
onChange={(e) =>
|
||||
setPasswordForm({
|
||||
...passwordForm,
|
||||
password_confirmation: e.target.value,
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md">
|
||||
<li>
|
||||
<Button type="submit" loading={loading}>
|
||||
Changer le mot de passe
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => setChangingPassword(false)}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
) : (
|
||||
<Button variant="secondary" onClick={() => setChangingPassword(true)}>
|
||||
Changer mon mot de passe
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setEditing(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => {
|
||||
setForm({ nom: user?.nom || '', prenom: user?.prenom || '', email: user?.email || '' });
|
||||
setEditing(true);
|
||||
}}>
|
||||
Modifier mes informations
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Changer mot de passe */}
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Lock size={20} className="text-gray-400" />
|
||||
<h3 className="font-display font-semibold text-gray-900 dark:text-white">
|
||||
Mot de passe
|
||||
</h3>
|
||||
{/* Zone dangereuse — RGPD */}
|
||||
<div className="app-panel fr-p-3w">
|
||||
<h2 className="fr-h5">
|
||||
<span className="fr-icon-warning-line fr-mr-1w" aria-hidden="true" />
|
||||
Zone dangereuse
|
||||
</h2>
|
||||
<p className="fr-text--sm">
|
||||
La suppression de votre compte est irréversible. Toutes vos données
|
||||
seront effacées conformément au RGPD.
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="fr-icon-delete-line fr-btn--icon-left"
|
||||
onClick={() => setDeleteModalOpen(true)}
|
||||
>
|
||||
Supprimer mon compte
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Modal confirmation suppression */}
|
||||
<Modal
|
||||
isOpen={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
title="Confirmer la suppression"
|
||||
size="sm"
|
||||
>
|
||||
<p>
|
||||
Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est
|
||||
irréversible.
|
||||
</p>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md">
|
||||
<li>
|
||||
<Button variant="secondary" onClick={handleDeleteAccount} loading={loading}>
|
||||
Oui, supprimer
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="secondary" onClick={() => setDeleteModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
{changingPassword ? (
|
||||
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||
<Input
|
||||
label="Mot de passe actuel"
|
||||
type="password"
|
||||
value={passwordForm.ancien_mot_de_passe}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, ancien_mot_de_passe: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Nouveau mot de passe"
|
||||
type="password"
|
||||
value={passwordForm.password}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, password: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
label="Confirmer le nouveau mot de passe"
|
||||
type="password"
|
||||
value={passwordForm.password_confirmation}
|
||||
onChange={(e) => setPasswordForm({ ...passwordForm, password_confirmation: e.target.value })}
|
||||
required
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<Button type="submit" loading={loading}>
|
||||
Changer le mot de passe
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setChangingPassword(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<Button variant="outline" onClick={() => setChangingPassword(true)}>
|
||||
Changer mon mot de passe
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Zone dangereuse */}
|
||||
<Card className="border-red-200 dark:border-red-900">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Shield size={20} className="text-red-500" />
|
||||
<h3 className="font-display font-semibold text-red-600 dark:text-red-400">
|
||||
Zone dangereuse
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
La suppression de votre compte est irréversible. Toutes vos données seront effacées conformément au RGPD.
|
||||
</p>
|
||||
<Button variant="danger" onClick={() => setDeleteModalOpen(true)}>
|
||||
<Trash2 size={16} />
|
||||
Supprimer mon compte
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* Modal confirmation suppression */}
|
||||
<Modal
|
||||
isOpen={deleteModalOpen}
|
||||
onClose={() => setDeleteModalOpen(false)}
|
||||
title="Confirmer la suppression"
|
||||
size="sm"
|
||||
>
|
||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||
Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.
|
||||
</p>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="danger" onClick={handleDeleteAccount} loading={loading}>
|
||||
Oui, supprimer
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={() => setDeleteModalOpen(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import { Phone } from 'lucide-react';
|
||||
|
||||
export default function ContactsUrgencePage() {
|
||||
const contacts = [
|
||||
{
|
||||
@@ -19,13 +16,13 @@ export default function ContactsUrgencePage() {
|
||||
name: 'Fil Santé Jeunes',
|
||||
number: '0 800 235 236',
|
||||
description:
|
||||
'Ligne d\u2019écoute gratuite et anonyme pour les jeunes de 12 à 25 ans. Du lundi au dimanche de 9h à 23h.',
|
||||
'Ligne d’écoute gratuite et anonyme pour les jeunes de 12 à 25 ans. Du lundi au dimanche de 9h à 23h.',
|
||||
},
|
||||
{
|
||||
name: 'SAMU',
|
||||
number: '15',
|
||||
description:
|
||||
'Service d\u2019aide médicale urgente. En cas d\u2019urgence vitale, appelez immédiatement le 15 ou le 112.',
|
||||
'Service d’aide médicale urgente. En cas d’urgence vitale, appelez immédiatement le 15 ou le 112.',
|
||||
},
|
||||
{
|
||||
name: 'SOS Médecins',
|
||||
@@ -36,48 +33,51 @@ export default function ContactsUrgencePage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
<h1 className="font-display text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Contacts d'urgence
|
||||
</h1>
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 mb-8">
|
||||
Si vous ou un proche traversez une période difficile, n'hésitez pas à
|
||||
contacter l'un de ces services. Vous n'êtes pas seul(e).
|
||||
<div className="fr-container fr-py-6w">
|
||||
<h1>Contacts d'urgence</h1>
|
||||
<p className="fr-text--lead">
|
||||
Si vous ou un proche traversez une période difficile, n'hésitez pas
|
||||
à contacter l'un de ces services. Vous n'êtes pas seul(e).
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{contacts.map((contact) => (
|
||||
<Card key={contact.number}>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-12 h-12 rounded-full bg-red-50 dark:bg-red-950/50 flex items-center justify-center">
|
||||
<Phone size={20} className="text-red-600 dark:text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h2 className="font-display text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
||||
{contact.name}
|
||||
</h2>
|
||||
<a
|
||||
href={`tel:${contact.number.replace(/\s/g, '')}`}
|
||||
className="inline-block text-xl font-bold text-malachite-600 dark:text-malachite-400 mb-2 hover:underline"
|
||||
>
|
||||
{contact.number}
|
||||
</a>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 leading-relaxed">
|
||||
{contact.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
<div className="fr-callout fr-callout--brown-caramel fr-icon-information-line fr-mb-6w">
|
||||
<p className="fr-callout__title">En cas d'urgence ou de détresse</p>
|
||||
<p className="fr-callout__text">
|
||||
Appelez le <strong>3114</strong> (numéro national de prévention du
|
||||
suicide, gratuit, 24h/24) ou le <strong>15</strong> (SAMU). En cas
|
||||
d'urgence vitale, composez le <strong>112</strong> (numéro
|
||||
d'urgence européen).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card className="mt-8 bg-red-50 dark:bg-red-950/30 border-red-100 dark:border-red-900">
|
||||
<p className="text-sm text-red-700 dark:text-red-400 text-center leading-relaxed">
|
||||
<strong>En cas d'urgence vitale</strong>, appelez immédiatement le{' '}
|
||||
<strong>15</strong> (SAMU) ou le <strong>112</strong> (numéro d'urgence
|
||||
européen).
|
||||
</p>
|
||||
</Card>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{contacts.map((contact) => (
|
||||
<div className="fr-col-12 fr-col-md-6" key={contact.number}>
|
||||
<div className="fr-card">
|
||||
<div className="fr-card__body">
|
||||
<div className="fr-card__content">
|
||||
<h2 className="fr-card__title">{contact.name}</h2>
|
||||
<p className="fr-card__desc">{contact.description}</p>
|
||||
<div className="fr-card__end">
|
||||
<p className="fr-card__detail">
|
||||
<a
|
||||
className="fr-link"
|
||||
href={`tel:${contact.number.replace(/\s/g, '')}`}
|
||||
>
|
||||
<span
|
||||
className="fr-icon-phone-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{contact.number}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
import { useParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { ArrowLeft, Calendar, User } from 'lucide-react';
|
||||
import { useFeed } from '@/hooks/useFeeds';
|
||||
import Skeleton from '@/components/ui/Skeleton';
|
||||
|
||||
export default function ArticlePage() {
|
||||
const params = useParams();
|
||||
@@ -14,79 +11,93 @@ export default function ArticlePage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
<Skeleton className="h-8 w-48 mb-8" />
|
||||
<Skeleton variant="rect" className="h-64 mb-6" />
|
||||
<Skeleton className="h-10 w-3/4 mb-4" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<div className="fr-container fr-py-6w">
|
||||
<p className="fr-text--sm">Chargement de l'article…</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !feed) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12 text-center">
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 mb-4">
|
||||
Article introuvable
|
||||
</p>
|
||||
<Link
|
||||
href="/informations"
|
||||
className="text-malachite-600 dark:text-malachite-400 hover:underline"
|
||||
>
|
||||
← Retour aux informations
|
||||
<div className="fr-container fr-py-6w">
|
||||
<div className="fr-alert fr-alert--error">
|
||||
<h3 className="fr-alert__title">Article introuvable</h3>
|
||||
<p>
|
||||
L'article demandé n'existe pas ou n'est plus
|
||||
disponible.
|
||||
</p>
|
||||
</div>
|
||||
<Link className="fr-link fr-mt-2w" href="/informations">
|
||||
Retour aux informations
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
<Link
|
||||
href="/informations"
|
||||
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-8 transition-colors"
|
||||
<div className="fr-container fr-py-6w">
|
||||
<nav
|
||||
role="navigation"
|
||||
className="fr-breadcrumb"
|
||||
aria-label="vous êtes ici :"
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
Retour aux informations
|
||||
</Link>
|
||||
|
||||
{feed.image_url && (
|
||||
<div className="relative w-full h-64 sm:h-80 rounded-2xl overflow-hidden bg-gray-100 dark:bg-gray-800 mb-8">
|
||||
<Image
|
||||
src={feed.image_url}
|
||||
alt={feed.titre}
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-full h-full object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="fr-breadcrumb__button"
|
||||
aria-expanded="false"
|
||||
aria-controls="breadcrumb-article"
|
||||
>
|
||||
Voir le fil d'Ariane
|
||||
</button>
|
||||
<div className="fr-collapse" id="breadcrumb-article">
|
||||
<ol className="fr-breadcrumb__list">
|
||||
<li>
|
||||
<Link className="fr-breadcrumb__link" href="/">
|
||||
Accueil
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link className="fr-breadcrumb__link" href="/informations">
|
||||
Informations
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a className="fr-breadcrumb__link" aria-current="page">
|
||||
{feed.titre}
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<h1 className="font-display text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
{feed.titre}
|
||||
</h1>
|
||||
<h1>{feed.titre}</h1>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400 dark:text-gray-500 mb-8 pb-8 border-b border-gray-200 dark:border-gray-800">
|
||||
<p className="fr-text--sm">
|
||||
{feed.auteur && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<User size={14} />
|
||||
<>
|
||||
<span
|
||||
className="fr-icon-user-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{feed.auteur.prenom} {feed.auteur.nom}
|
||||
</div>
|
||||
{' — '}
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Calendar size={14} />
|
||||
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className="fr-icon-calendar-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
|
||||
<article
|
||||
className="prose prose-lg dark:prose-invert max-w-none prose-headings:font-display prose-a:text-malachite-600 dark:prose-a:text-malachite-400"
|
||||
<hr className="fr-mt-2w fr-mb-4w" />
|
||||
|
||||
<div
|
||||
className="fr-col-12 fr-col-md-10"
|
||||
dangerouslySetInnerHTML={{ __html: feed.contenu }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,86 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { BookOpen, Calendar } from 'lucide-react';
|
||||
import { useFeeds } from '@/hooks/useFeeds';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Skeleton from '@/components/ui/Skeleton';
|
||||
|
||||
export default function InformationsPage() {
|
||||
const { data: feeds, isLoading } = useFeeds();
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto px-4 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="font-display text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Informations & Ressources
|
||||
</h1>
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Des contenus validés sur la santé mentale, la gestion du stress
|
||||
et le bien-être émotionnel.
|
||||
</p>
|
||||
</div>
|
||||
<div className="fr-container fr-py-6w">
|
||||
<h1>Informations & Ressources</h1>
|
||||
<p className="fr-text--lead">
|
||||
Des contenus validés sur la santé mentale, la gestion du stress et le
|
||||
bien-être émotionnel.
|
||||
</p>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<Card key={i}>
|
||||
<Skeleton variant="rect" className="h-48 mb-4" />
|
||||
<Skeleton className="h-6 w-3/4 mb-2" />
|
||||
<Skeleton className="h-4 w-full mb-1" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<p className="fr-text--sm">Chargement des articles…</p>
|
||||
) : feeds && feeds.length > 0 ? (
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{feeds.map((feed) => (
|
||||
<Link key={feed.id} href={`/informations/${feed.slug}`}>
|
||||
<Card hover className="h-full">
|
||||
{feed.image_url && (
|
||||
<div className="relative w-full h-48 mb-4 rounded-xl overflow-hidden bg-gray-100 dark:bg-gray-800">
|
||||
<Image
|
||||
src={feed.image_url}
|
||||
alt={feed.titre}
|
||||
width={800}
|
||||
height={400}
|
||||
className="w-full h-full object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="fr-col-12 fr-col-md-6" key={feed.id}>
|
||||
<div className="fr-card fr-enlarge-link">
|
||||
<div className="fr-card__body">
|
||||
<div className="fr-card__content">
|
||||
<h2 className="fr-card__title">
|
||||
<Link href={`/informations/${feed.slug}`}>
|
||||
{feed.titre}
|
||||
</Link>
|
||||
</h2>
|
||||
<p className="fr-card__desc">
|
||||
{(feed.contenu ?? '')
|
||||
.replace(/<[^>]*>/g, '')
|
||||
.substring(0, 200) + '…'}
|
||||
</p>
|
||||
<div className="fr-card__end">
|
||||
<p className="fr-card__detail">
|
||||
<span
|
||||
className="fr-icon-calendar-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BookOpen size={16} className="text-malachite-500" />
|
||||
<span className="text-xs font-medium text-malachite-600 dark:text-malachite-400 uppercase tracking-wide">
|
||||
Article
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-2 line-clamp-2">
|
||||
{feed.titre}
|
||||
</h2>
|
||||
<div
|
||||
className="text-gray-500 dark:text-gray-400 text-sm line-clamp-3 mb-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: (feed.contenu ?? '').replace(/<[^>]*>/g, '').substring(0, 200) + '...',
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
<Calendar size={14} />
|
||||
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16 text-gray-400 dark:text-gray-600">
|
||||
<BookOpen size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg">Aucun article disponible pour le moment</p>
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__text">
|
||||
Aucun article disponible pour le moment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,73 +1,47 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
|
||||
export default function MentionsLegalesPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
<h1 className="font-display text-4xl font-bold text-gray-900 dark:text-white mb-8">
|
||||
Mentions légales
|
||||
</h1>
|
||||
<div className="fr-container fr-py-6w">
|
||||
<h1>Mentions légales</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Éditeur de l'application
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
CESIZen est une application développée dans le cadre d'un projet
|
||||
pédagogique au sein de{' '}
|
||||
<strong>CESI École d'Ingénieurs</strong>. Cette application est
|
||||
réalisée à but éducatif et n'a pas de vocation commerciale.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Éditeur de l'application</h2>
|
||||
<p>
|
||||
CESIZen est une application développée dans le cadre d'un projet
|
||||
pédagogique au sein de <strong>CESI École d'Ingénieurs</strong>.
|
||||
Cette application est réalisée à but éducatif et n'a pas de vocation
|
||||
commerciale.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Hébergement
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
L'application est hébergée dans le cadre de l'infrastructure
|
||||
pédagogique de CESI. Les données sont stockées sur des serveurs
|
||||
sécurisés situés en France.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Hébergement</h2>
|
||||
<p>
|
||||
L'application est hébergée dans le cadre de l'infrastructure
|
||||
pédagogique de CESI. Les données sont stockées sur des serveurs
|
||||
sécurisés situés en France.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Propriété intellectuelle
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
L'ensemble des contenus présents sur CESIZen (textes, images,
|
||||
graphismes, logo, icônes) sont la propriété exclusive de leurs auteurs
|
||||
respectifs et sont protégés par les lois françaises et internationales
|
||||
relatives à la propriété intellectuelle.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Propriété intellectuelle</h2>
|
||||
<p>
|
||||
L'ensemble des contenus présents sur CESIZen (textes, images,
|
||||
graphismes, logo, icônes) sont la propriété exclusive de leurs auteurs
|
||||
respectifs et sont protégés par les lois françaises et internationales
|
||||
relatives à la propriété intellectuelle.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Responsabilité
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
CESIZen est un outil de suivi du bien-être et ne se substitue en aucun
|
||||
cas à un avis médical professionnel. En cas de détresse psychologique,
|
||||
veuillez contacter les services d'urgence ou un professionnel de
|
||||
santé.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Responsabilité</h2>
|
||||
<p>
|
||||
CESIZen est un outil de suivi du bien-être et ne se substitue en aucun
|
||||
cas à un avis médical professionnel. En cas de détresse psychologique,
|
||||
veuillez contacter les services d'urgence ou un professionnel de
|
||||
santé.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Contact
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Pour toute question relative aux mentions légales, vous pouvez nous
|
||||
contacter via l'adresse suivante :{' '}
|
||||
<span className="text-malachite-600 dark:text-malachite-400">
|
||||
contact@cesizen.fr
|
||||
</span>
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
<h2>Contact</h2>
|
||||
<p>
|
||||
Pour toute question relative aux mentions légales, vous pouvez nous
|
||||
contacter via l'adresse suivante :{' '}
|
||||
<a className="fr-link" href="mailto:contact@cesizen.fr">
|
||||
contact@cesizen.fr
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
{/* ─── Hero ─── */}
|
||||
<section className="fr-container fr-py-8w">
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-grid-row--middle">
|
||||
<div className="fr-col-12 fr-col-md-7">
|
||||
<h1>
|
||||
CESIZen, prenez soin de votre santé mentale
|
||||
</h1>
|
||||
<p className="fr-text--lead">
|
||||
Une plateforme publique pour suivre vos émotions au quotidien,
|
||||
comprendre vos tendances et accéder à des ressources fiables sur la
|
||||
prévention du stress.
|
||||
</p>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md">
|
||||
<li>
|
||||
<Link className="fr-btn fr-btn--lg" href="/inscription">
|
||||
Créer un compte gratuitement
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="fr-btn fr-btn--lg fr-btn--secondary"
|
||||
href="/informations"
|
||||
>
|
||||
Consulter les ressources
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="fr-col-12 fr-col-md-5">
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__title">Pourquoi suivre ses émotions ?</p>
|
||||
<p className="fr-callout__text">
|
||||
Nommer une émotion réduit son intensité (affect labeling, UCLA).
|
||||
Le suivi régulier aide à identifier ses tendances et à mieux gérer
|
||||
son stress.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Comment ça marche ─── */}
|
||||
<section className="fr-background-alt--grey fr-py-8w">
|
||||
<div className="fr-container">
|
||||
<h2>Comment ça marche ?</h2>
|
||||
<p className="fr-text--lead fr-mb-6w">
|
||||
Trois étapes simples pour prendre soin de votre bien-être émotionnel.
|
||||
</p>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{[
|
||||
{
|
||||
step: "1",
|
||||
title: "Identifiez votre émotion",
|
||||
desc: "Choisissez parmi un référentiel d'émotions et sous-émotions, puis notez l'intensité ressentie de 1 à 10.",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
title: "Ajoutez du contexte",
|
||||
desc: "Notez ce qui a déclenché cette émotion. Quelques mots suffisent à mieux comprendre vos tendances.",
|
||||
},
|
||||
{
|
||||
step: "3",
|
||||
title: "Suivez votre évolution",
|
||||
desc: "Consultez des rapports visuels (semaine, mois, trimestre, année) pour identifier vos tendances.",
|
||||
},
|
||||
].map((s) => (
|
||||
<div className="fr-col-12 fr-col-md-4" key={s.step}>
|
||||
<div className="fr-card fr-enlarge-link">
|
||||
<div className="fr-card__body">
|
||||
<div className="fr-card__content">
|
||||
<h3 className="fr-card__title">
|
||||
<span className="fr-badge fr-badge--blue-ecume fr-mb-1w">
|
||||
Étape {s.step}
|
||||
</span>
|
||||
<br />
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="fr-card__desc">{s.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Fonctionnalités ─── */}
|
||||
<section className="fr-container fr-py-8w">
|
||||
<h2>Tout ce dont vous avez besoin</h2>
|
||||
<p className="fr-text--lead fr-mb-6w">
|
||||
Des outils conçus pour vous accompagner dans la gestion de votre
|
||||
bien-être émotionnel.
|
||||
</p>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{[
|
||||
{
|
||||
icon: "fr-icon-heart-line",
|
||||
title: "Journal d'émotions",
|
||||
desc: "Enregistrez vos émotions quotidiennes et suivez leur évolution dans un journal sécurisé.",
|
||||
},
|
||||
{
|
||||
icon: "fr-icon-line-chart-line",
|
||||
title: "Rapports visuels",
|
||||
desc: "Visualisez vos tendances : répartition, évolution et intensité de vos émotions.",
|
||||
},
|
||||
{
|
||||
icon: "fr-icon-book-2-line",
|
||||
title: "Ressources fiables",
|
||||
desc: "Accédez à des contenus validés sur la santé mentale et la gestion du stress.",
|
||||
},
|
||||
{
|
||||
icon: "fr-icon-shield-line",
|
||||
title: "Données protégées",
|
||||
desc: "Vos données sont protégées conformément au RGPD. Vous en gardez le contrôle total.",
|
||||
},
|
||||
].map((f) => (
|
||||
<div className="fr-col-12 fr-col-md-6 fr-col-lg-3" key={f.title}>
|
||||
<div className="fr-tile fr-tile--vertical">
|
||||
<div className="fr-tile__body">
|
||||
<div className="fr-tile__content">
|
||||
<h3 className="fr-tile__title">
|
||||
<span
|
||||
className={`${f.icon} fr-mr-1w`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{f.title}
|
||||
</h3>
|
||||
<p className="fr-tile__detail">{f.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Engagements ─── */}
|
||||
<section className="fr-background-alt--grey fr-py-8w">
|
||||
<div className="fr-container">
|
||||
<h2>Une application pensée pour vous</h2>
|
||||
<p className="fr-text--lead fr-mb-4w">
|
||||
CESIZen est édité pour le Ministère des Solidarités et de la Santé :
|
||||
un outil fiable, accessible et respectueux de votre vie privée.
|
||||
</p>
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{[
|
||||
"Totalement gratuit et sans publicité",
|
||||
"Conforme RGPD — vos données vous appartiennent",
|
||||
"Interface accessible et inclusive (RGAA)",
|
||||
"Rapports visuels pour comprendre vos tendances",
|
||||
"Contenus validés par des professionnels de santé",
|
||||
"Export et suppression de vos données à tout moment",
|
||||
].map((item) => (
|
||||
<div className="fr-col-12 fr-col-md-6" key={item}>
|
||||
<p>
|
||||
<span
|
||||
className="fr-icon-check-line fr-mr-1w"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md fr-mt-4w">
|
||||
<li>
|
||||
<Link className="fr-btn fr-btn--lg" href="/inscription">
|
||||
Créer mon compte
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="fr-btn fr-btn--lg fr-btn--secondary"
|
||||
href="/connexion"
|
||||
>
|
||||
Se connecter
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Urgence ─── */}
|
||||
<section className="fr-container fr-py-6w">
|
||||
<div className="fr-callout fr-callout--brown-caramel fr-icon-information-line">
|
||||
<p className="fr-callout__title">En cas d'urgence ou de détresse</p>
|
||||
<p className="fr-callout__text">
|
||||
Appelez le <strong>3114</strong> (numéro national de prévention du
|
||||
suicide, gratuit, 24h/24) ou le <strong>15</strong> (SAMU). Vous
|
||||
n'êtes pas seul·e.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,105 +1,73 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
|
||||
export default function PolitiqueConfidentialitePage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 py-12">
|
||||
<h1 className="font-display text-4xl font-bold text-gray-900 dark:text-white mb-8">
|
||||
Politique de confidentialité
|
||||
</h1>
|
||||
<div className="fr-container fr-py-6w">
|
||||
<h1>Politique de confidentialité</h1>
|
||||
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Conformité RGPD
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
CESIZen s'engage à respecter le Règlement Général sur la Protection
|
||||
des Données (RGPD) et la loi Informatique et Libertés. La protection
|
||||
de vos données personnelles est une priorité.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Conformité RGPD</h2>
|
||||
<p>
|
||||
CESIZen s'engage à respecter le Règlement Général sur la Protection
|
||||
des Données (RGPD) et la loi Informatique et Libertés. La protection de
|
||||
vos données personnelles est une priorité.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Données collectées
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
|
||||
Dans le cadre de l'utilisation de CESIZen, les données suivantes
|
||||
sont collectées :
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li>Adresse e-mail</li>
|
||||
<li>Nom et prénom</li>
|
||||
<li>Données relatives aux émotions enregistrées</li>
|
||||
</ul>
|
||||
</Card>
|
||||
<h2>Données collectées</h2>
|
||||
<p>
|
||||
Dans le cadre de l'utilisation de CESIZen, les données suivantes
|
||||
sont collectées :
|
||||
</p>
|
||||
<ul>
|
||||
<li>Adresse e-mail</li>
|
||||
<li>Nom et prénom</li>
|
||||
<li>Données relatives aux émotions enregistrées</li>
|
||||
</ul>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Finalité du traitement
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Ces données sont collectées uniquement dans le but de fournir un suivi
|
||||
personnalisé du bien-être émotionnel. Elles ne sont ni vendues, ni
|
||||
partagées avec des tiers à des fins commerciales.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Finalité du traitement</h2>
|
||||
<p>
|
||||
Ces données sont collectées uniquement dans le but de fournir un suivi
|
||||
personnalisé du bien-être émotionnel. Elles ne sont ni vendues, ni
|
||||
partagées avec des tiers à des fins commerciales.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Durée de conservation
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Les données personnelles sont conservées pendant toute la durée
|
||||
d'utilisation du compte. En cas de suppression du compte, les
|
||||
données sont effacées dans un délai de 30 jours.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Durée de conservation</h2>
|
||||
<p>
|
||||
Les données personnelles sont conservées pendant toute la durée
|
||||
d'utilisation du compte. En cas de suppression du compte, les
|
||||
données sont effacées dans un délai de 30 jours.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Sécurité des données
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Les données sont chiffrées en transit (HTTPS) et au repos. Les mots de
|
||||
passe sont hachés avec des algorithmes sécurisés. L'accès aux
|
||||
données est strictement limité aux personnes habilitées.
|
||||
</p>
|
||||
</Card>
|
||||
<h2>Sécurité des données</h2>
|
||||
<p>
|
||||
Les données sont chiffrées en transit (HTTPS) et au repos. Les mots de
|
||||
passe sont hachés avec des algorithmes sécurisés. L'accès aux
|
||||
données est strictement limité aux personnes habilitées.
|
||||
</p>
|
||||
|
||||
<Card>
|
||||
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
||||
Vos droits
|
||||
</h2>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed mb-3">
|
||||
Conformément au RGPD, vous disposez des droits suivants :
|
||||
</p>
|
||||
<ul className="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li>
|
||||
<strong>Droit d'accès :</strong> obtenir une copie de vos
|
||||
données personnelles
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit de rectification :</strong> corriger des données
|
||||
inexactes ou incomplètes
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit de suppression :</strong> demander l'effacement
|
||||
de vos données personnelles
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit à la portabilité :</strong> récupérer vos données dans
|
||||
un format structuré
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed mt-3">
|
||||
Pour exercer ces droits, contactez-nous à l'adresse :{' '}
|
||||
<span className="text-malachite-600 dark:text-malachite-400">
|
||||
contact@cesizen.fr
|
||||
</span>
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
<h2>Vos droits</h2>
|
||||
<p>Conformément au RGPD, vous disposez des droits suivants :</p>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Droit d'accès :</strong> obtenir une copie de vos données
|
||||
personnelles
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit de rectification :</strong> corriger des données
|
||||
inexactes ou incomplètes
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit de suppression :</strong> demander l'effacement de
|
||||
vos données personnelles
|
||||
</li>
|
||||
<li>
|
||||
<strong>Droit à la portabilité :</strong> récupérer vos données dans
|
||||
un format structuré
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Pour exercer ces droits, contactez-nous à l'adresse :{' '}
|
||||
<a className="fr-link" href="mailto:contact@cesizen.fr">
|
||||
contact@cesizen.fr
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Panneau neutre (remplace l'usage détourné de .fr-card comme conteneur).
|
||||
Utilise les variables DSFR pour s'adapter aux thèmes clair/sombre. */
|
||||
.app-panel {
|
||||
background-color: var(--background-default-grey);
|
||||
border: 1px solid var(--border-default-grey);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
||||
+20
-19
@@ -1,26 +1,17 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Poppins } from "next/font/google";
|
||||
import Script from "next/script";
|
||||
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",
|
||||
title: "CESIZen - Votre santé mentale",
|
||||
description:
|
||||
"Plateforme de gestion du stress et suivi \u00e9motionnel. Suivez vos \u00e9motions, consultez des ressources et prenez soin de votre sant\u00e9 mentale.",
|
||||
"Plateforme publique de gestion du stress et de suivi émotionnel. Suivez vos émotions, consultez des ressources et prenez soin de votre santé mentale.",
|
||||
icons: {
|
||||
icon: [{ url: "/dsfr/favicon/favicon.svg", type: "image/svg+xml" }],
|
||||
shortcut: ["/dsfr/favicon/favicon.ico"],
|
||||
apple: [{ url: "/dsfr/favicon/apple-touch-icon.png" }],
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -29,9 +20,19 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr" suppressHydrationWarning>
|
||||
<body className={`${inter.variable} ${poppins.variable} antialiased`}>
|
||||
<html lang="fr" data-fr-scheme="system" suppressHydrationWarning>
|
||||
<body>
|
||||
{/* Système de Design de l'État (DSFR) — feuilles de style officielles */}
|
||||
<link rel="stylesheet" href="/dsfr/dsfr.min.css" />
|
||||
<link rel="stylesheet" href="/dsfr/utility/utility.min.css" />
|
||||
<Providers>{children}</Providers>
|
||||
{/* JS DSFR : comportements interactifs (menu, navigation, modales) */}
|
||||
<Script src="/dsfr/dsfr.module.min.js" type="module" strategy="afterInteractive" />
|
||||
<Script
|
||||
src="/dsfr/dsfr.nomodule.min.js"
|
||||
noModule
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,592 +0,0 @@
|
||||
'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<HTMLDivElement>(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 (
|
||||
<div
|
||||
ref={ref}
|
||||
className={`reveal-section ${className}`}
|
||||
style={{ transitionDelay: `${delay}ms` }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="min-h-screen overflow-hidden">
|
||||
<style jsx global>{`
|
||||
.reveal-section {
|
||||
opacity: 0;
|
||||
transform: translateY(40px);
|
||||
transition: opacity 0.8s cubic-bezier(0.16, 1, 0.3, 1), transform 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.reveal-section.animate-in {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(2deg); }
|
||||
}
|
||||
@keyframes float-delay {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-15px) rotate(-2deg); }
|
||||
}
|
||||
@keyframes gradient-shift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
@keyframes pulse-ring {
|
||||
0% { transform: scale(0.8); opacity: 0.5; }
|
||||
50% { transform: scale(1.1); opacity: 0; }
|
||||
100% { transform: scale(0.8); opacity: 0; }
|
||||
}
|
||||
@keyframes count-up {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.float-anim { animation: float 6s ease-in-out infinite; }
|
||||
.float-anim-delay { animation: float-delay 7s ease-in-out infinite; }
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #06c656, #fce117, #06c656);
|
||||
background-size: 200% 200%;
|
||||
animation: gradient-shift 4s ease infinite;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* ─── Hero Section ─── */}
|
||||
<section className="relative overflow-hidden bg-linear-to-br from-candlelight-50 via-white to-malachite-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 min-h-[90vh] flex items-center">
|
||||
{/* Orbes décoratives */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-125 h-125 bg-candlelight-200/40 dark:bg-candlelight-500/10 rounded-full blur-[100px] float-anim" />
|
||||
<div className="absolute -bottom-40 -left-40 w-125 h-125 bg-malachite-200/40 dark:bg-malachite-500/10 rounded-full blur-[100px] float-anim-delay" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-150 h-150 bg-candlelight-100/20 dark:bg-candlelight-500/5 rounded-full blur-[120px]" />
|
||||
</div>
|
||||
|
||||
{/* Grille de fond */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(0,0,0,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(0,0,0,0.03)_1px,transparent_1px)] dark:bg-[linear-gradient(rgba(255,255,255,0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.03)_1px,transparent_1px)] bg-size-[60px_60px] pointer-events-none" />
|
||||
|
||||
<div className="relative max-w-6xl mx-auto px-4 py-20 sm:py-32 w-full">
|
||||
<div className="text-center">
|
||||
<RevealSection>
|
||||
<div className="inline-flex items-center gap-2 bg-white/70 dark:bg-gray-800/70 backdrop-blur-md rounded-full px-5 py-2.5 mb-8 border border-gray-200/80 dark:border-gray-700/80 shadow-sm">
|
||||
<Sparkles className="w-4 h-4 text-candlelight-500" />
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Plateforme du Ministère des Solidarités et de la Santé
|
||||
</span>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={100}>
|
||||
<h1 className="font-display text-6xl sm:text-7xl lg:text-8xl font-bold tracking-tight mb-6">
|
||||
<span className="gradient-text">CESI</span>
|
||||
<span className="text-gray-900 dark:text-white">ZEN</span>
|
||||
</h1>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={200}>
|
||||
<p className="text-xl sm:text-2xl lg:text-3xl text-gray-600 dark:text-gray-300 max-w-3xl mx-auto mb-4 font-light">
|
||||
Votre compagnon pour une{' '}
|
||||
<span className="font-semibold text-malachite-600 dark:text-malachite-400">santé mentale</span>{' '}
|
||||
épanouie
|
||||
</p>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={300}>
|
||||
<p className="text-base sm:text-lg text-gray-500 dark:text-gray-400 max-w-xl mx-auto mb-12 leading-relaxed">
|
||||
Suivez vos émotions, comprenez vos tendances et prenez soin de vous
|
||||
au quotidien avec des outils simples, bienveillants et confidentiels.
|
||||
</p>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={400}>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center mb-16">
|
||||
<Link
|
||||
href="/inscription"
|
||||
className="group inline-flex items-center justify-center gap-2 bg-candlelight-500 hover:bg-candlelight-400 text-black font-semibold px-8 py-4 rounded-2xl transition-all duration-300 hover:shadow-xl hover:shadow-candlelight-500/30 hover:-translate-y-0.5 text-lg"
|
||||
>
|
||||
Commencer gratuitement
|
||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/informations"
|
||||
className="inline-flex items-center justify-center gap-2 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm hover:bg-white dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-semibold px-8 py-4 rounded-2xl border-2 border-gray-200 dark:border-gray-700 transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 text-lg"
|
||||
>
|
||||
<BookOpen className="w-5 h-5" />
|
||||
En savoir plus
|
||||
</Link>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
{/* Mini stats */}
|
||||
<RevealSection delay={500}>
|
||||
<div className="flex flex-wrap justify-center gap-8 sm:gap-12">
|
||||
<MiniStat icon={<Users className="w-5 h-5" />} value="100%" label="Gratuit" />
|
||||
<MiniStat icon={<Shield className="w-5 h-5" />} value="RGPD" label="Conforme" />
|
||||
<MiniStat icon={<Clock className="w-5 h-5" />} value="2 min" label="Pour commencer" />
|
||||
</div>
|
||||
</RevealSection>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Séparateur vague */}
|
||||
<div className="absolute bottom-0 left-0 right-0">
|
||||
<svg viewBox="0 0 1440 80" fill="none" className="w-full h-auto" preserveAspectRatio="none">
|
||||
<path d="M0 80L60 68C120 56 240 32 360 24C480 16 600 24 720 32C840 40 960 48 1080 44C1200 40 1320 24 1380 16L1440 8V80H1380C1320 80 1200 80 1080 80C960 80 840 80 720 80C600 80 480 80 360 80C240 80 120 80 60 80H0Z" className="fill-white dark:fill-gray-950" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Comment ça marche ─── */}
|
||||
<section className="py-24 bg-white dark:bg-gray-950">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<RevealSection>
|
||||
<div className="text-center mb-20">
|
||||
<span className="inline-flex items-center gap-2 text-malachite-600 dark:text-malachite-400 font-semibold text-sm uppercase tracking-wider mb-4">
|
||||
<Zap className="w-4 h-4" />
|
||||
Simple et rapide
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Comment ça marche ?
|
||||
</h2>
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Trois étapes simples pour prendre soin de votre bien-être émotionnel.
|
||||
</p>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 lg:gap-12 relative">
|
||||
{/* Ligne de connexion */}
|
||||
<div className="hidden md:block absolute top-24 left-[20%] right-[20%] h-0.5 bg-linear-to-r from-candlelight-300 via-malachite-300 to-candlelight-300 dark:from-candlelight-800 dark:via-malachite-800 dark:to-candlelight-800" />
|
||||
|
||||
<RevealSection delay={0}>
|
||||
<StepCard
|
||||
step={1}
|
||||
icon={<SmilePlus className="w-7 h-7" />}
|
||||
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"
|
||||
/>
|
||||
</RevealSection>
|
||||
<RevealSection delay={150}>
|
||||
<StepCard
|
||||
step={2}
|
||||
icon={<Brain className="w-7 h-7" />}
|
||||
title="Ajoutez du contexte"
|
||||
description="Notez ce qui a déclenché cette émotion. Même quelques mots suffisent à mieux comprendre vos tendances."
|
||||
color="malachite"
|
||||
/>
|
||||
</RevealSection>
|
||||
<RevealSection delay={300}>
|
||||
<StepCard
|
||||
step={3}
|
||||
icon={<TrendingUp className="w-7 h-7" />}
|
||||
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"
|
||||
/>
|
||||
</RevealSection>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Features Section ─── */}
|
||||
<section className="py-24 bg-gray-50 dark:bg-gray-900/50 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-candlelight-200/20 dark:bg-candlelight-500/5 rounded-full blur-[100px] pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 w-96 h-96 bg-malachite-200/20 dark:bg-malachite-500/5 rounded-full blur-[100px] pointer-events-none" />
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 relative">
|
||||
<RevealSection>
|
||||
<div className="text-center mb-20">
|
||||
<span className="inline-flex items-center gap-2 text-candlelight-600 dark:text-candlelight-400 font-semibold text-sm uppercase tracking-wider mb-4">
|
||||
<Star className="w-4 h-4" />
|
||||
Fonctionnalités
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Tout ce dont vous avez besoin
|
||||
</h2>
|
||||
<p className="text-lg text-gray-500 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
Des outils conçus pour vous accompagner au quotidien dans la gestion
|
||||
de votre bien-être émotionnel.
|
||||
</p>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{[
|
||||
{
|
||||
icon: <Heart className="w-7 h-7" />,
|
||||
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: <BarChart3 className="w-7 h-7" />,
|
||||
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: <BookOpen className="w-7 h-7" />,
|
||||
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: <Shield className="w-7 h-7" />,
|
||||
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) => (
|
||||
<RevealSection key={feature.title} delay={feature.delay}>
|
||||
<FeatureCard {...feature} />
|
||||
</RevealSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Témoignages / Social Proof ─── */}
|
||||
<section className="py-24 bg-white dark:bg-gray-950">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<RevealSection>
|
||||
<div className="text-center mb-16">
|
||||
<span className="inline-flex items-center gap-2 text-malachite-600 dark:text-malachite-400 font-semibold text-sm uppercase tracking-wider mb-4">
|
||||
<Heart className="w-4 h-4" />
|
||||
Pourquoi CESIZen ?
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl lg:text-5xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Conçu pour votre bien-être
|
||||
</h2>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<RevealSection key={item.author} delay={item.delay}>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-3xl p-8 h-full flex flex-col border border-gray-100 dark:border-gray-800 transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<div className="flex gap-1 mb-4">
|
||||
{[1, 2, 3, 4, 5].map((s) => (
|
||||
<Star key={s} className="w-4 h-4 fill-candlelight-400 text-candlelight-400" />
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-gray-700 dark:text-gray-300 leading-relaxed mb-6 flex-1 italic">
|
||||
“{item.quote}”
|
||||
</blockquote>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white text-sm">{item.author}</p>
|
||||
<p className="text-gray-500 dark:text-gray-500 text-xs mt-1">{item.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
</RevealSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Checklist Section ─── */}
|
||||
<section className="py-24 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="max-w-6xl mx-auto px-4">
|
||||
<div className="grid lg:grid-cols-2 gap-16 items-center">
|
||||
<RevealSection>
|
||||
<div>
|
||||
<span className="inline-flex items-center gap-2 text-candlelight-600 dark:text-candlelight-400 font-semibold text-sm uppercase tracking-wider mb-4">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
Avantages
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||
Une application pensée pour vous
|
||||
</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mb-8 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
'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) => (
|
||||
<div key={i} className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-malachite-500 mt-0.5 shrink-0" />
|
||||
<span className="text-gray-700 dark:text-gray-300">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={200}>
|
||||
<div className="relative">
|
||||
{/* Carte décorative simulant l'app */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-3xl shadow-2xl shadow-gray-200/50 dark:shadow-black/30 p-8 border border-gray-100 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 bg-candlelight-500 rounded-xl flex items-center justify-center">
|
||||
<Heart className="w-5 h-5 text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 dark:text-white text-sm">Comment vous sentez-vous ?</p>
|
||||
<p className="text-gray-500 text-xs">Aujourd'hui à 14:30</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3 mb-6">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={e.label} className={`${e.color} rounded-2xl p-3 text-center transition-transform hover:scale-105`}>
|
||||
<span className="text-2xl block mb-1">{e.emoji}</span>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">{e.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-3 mb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Intensité</span>
|
||||
<span className="text-sm font-semibold text-malachite-600 dark:text-malachite-400">7/10</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-3">
|
||||
<div className="bg-linear-to-r from-malachite-400 to-malachite-500 rounded-full h-3" style={{ width: '70%' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-2xl p-4">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
|
||||
“Belle journée au travail, bonne ambiance avec l'équipe...”
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Décoration */}
|
||||
<div className="absolute -z-10 -top-6 -right-6 w-full h-full bg-linear-to-br from-candlelight-200 to-malachite-200 dark:from-candlelight-900/30 dark:to-malachite-900/30 rounded-3xl" />
|
||||
</div>
|
||||
</RevealSection>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── CTA Section ─── */}
|
||||
<section className="py-24 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-linear-to-br from-malachite-500 via-malachite-600 to-malachite-700" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(rgba(255,255,255,0.05)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.05)_1px,transparent_1px)] bg-size-[40px_40px] pointer-events-none" />
|
||||
<div className="absolute -top-20 -right-20 w-80 h-80 bg-candlelight-400/20 rounded-full blur-[80px] pointer-events-none" />
|
||||
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-malachite-300/20 rounded-full blur-[80px] pointer-events-none" />
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 text-center relative">
|
||||
<RevealSection>
|
||||
<div className="inline-flex items-center gap-2 bg-white/15 backdrop-blur-sm rounded-full px-5 py-2 mb-8 border border-white/20">
|
||||
<Sparkles className="w-4 h-4 text-candlelight-300" />
|
||||
<span className="text-sm font-medium text-white/90">Rejoignez la communauté</span>
|
||||
</div>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={100}>
|
||||
<h2 className="font-display text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
|
||||
Prêt à prendre soin de vous ?
|
||||
</h2>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={200}>
|
||||
<p className="text-lg text-white/80 mb-10 max-w-2xl mx-auto leading-relaxed">
|
||||
Créez votre compte en 2 minutes et commencez à suivre votre bien-être
|
||||
émotionnel. C'est gratuit, confidentiel et sans engagement.
|
||||
</p>
|
||||
</RevealSection>
|
||||
|
||||
<RevealSection delay={300}>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Link
|
||||
href="/inscription"
|
||||
className="group inline-flex items-center gap-2 bg-candlelight-500 hover:bg-candlelight-400 text-black font-bold px-10 py-4 rounded-2xl transition-all duration-300 hover:shadow-xl hover:shadow-candlelight-500/30 hover:-translate-y-0.5 text-lg"
|
||||
>
|
||||
Créer mon compte
|
||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover:translate-x-1" />
|
||||
</Link>
|
||||
<Link
|
||||
href="/connexion"
|
||||
className="inline-flex items-center gap-2 bg-white/15 hover:bg-white/25 backdrop-blur-sm text-white font-semibold px-10 py-4 rounded-2xl border border-white/20 transition-all duration-300 hover:-translate-y-0.5 text-lg"
|
||||
>
|
||||
Se connecter
|
||||
</Link>
|
||||
</div>
|
||||
</RevealSection>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ─── Urgence Banner ─── */}
|
||||
<section className="bg-red-50 dark:bg-red-950/30 border-t border-red-100 dark:border-red-900/50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4 text-center">
|
||||
<p className="text-red-700 dark:text-red-300 font-medium leading-relaxed">
|
||||
<span className="font-bold">En cas d'urgence</span>, appelez le{' '}
|
||||
<a href="tel:3114" className="font-bold underline decoration-2 underline-offset-2 hover:text-red-900 dark:hover:text-red-200 transition-colors">
|
||||
3114
|
||||
</a>{' '}
|
||||
(numéro national de prévention du suicide) ou le{' '}
|
||||
<a href="tel:15" className="font-bold underline decoration-2 underline-offset-2 hover:text-red-900 dark:hover:text-red-200 transition-colors">
|
||||
15
|
||||
</a>{' '}
|
||||
(SAMU)
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Composants ─── */
|
||||
|
||||
function MiniStat({ icon, value, label }: { icon: React.ReactNode; value: string; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm rounded-xl flex items-center justify-center text-malachite-600 dark:text-malachite-400 shadow-sm border border-gray-200/50 dark:border-gray-700/50">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-gray-900 dark:text-white text-sm">{value}</p>
|
||||
<p className="text-gray-500 dark:text-gray-500 text-xs">{label}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`${colors.bg} rounded-3xl p-8 text-center relative transition-all duration-300 hover:shadow-xl hover:-translate-y-2 border border-transparent hover:border-gray-200 dark:hover:border-gray-700 h-full`}>
|
||||
<div className={`${colors.badge} text-white w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm absolute -top-3 left-1/2 -translate-x-1/2 shadow-lg`}>
|
||||
{step}
|
||||
</div>
|
||||
<div className={`${colors.iconBg} ${colors.icon} w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-5 mt-2`}>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="font-display font-semibold text-lg text-gray-900 dark:text-white mb-3">{title}</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`${bgColor} rounded-3xl p-7 transition-all duration-300 hover:shadow-xl hover:-translate-y-2 border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 h-full`}>
|
||||
<div className={`${iconBg} ${iconColor} w-14 h-14 rounded-2xl flex items-center justify-center mb-5`}>
|
||||
{icon}
|
||||
</div>
|
||||
<h3 className="font-display font-semibold text-lg text-gray-900 dark:text-white mb-3">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,63 +57,69 @@ export default function EditSaisieModal({ saisie, isOpen, onClose }: EditSaisieM
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Modifier la saisie" size="lg">
|
||||
<div className="space-y-6">
|
||||
{/* Émotion */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Émotion
|
||||
</label>
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
</div>
|
||||
{/* Émotion */}
|
||||
<div className="fr-input-group">
|
||||
<p className="fr-label">Émotion</p>
|
||||
<EmotionPicker value={emotionId} onChange={setEmotionId} />
|
||||
</div>
|
||||
|
||||
{/* Intensité */}
|
||||
<div>
|
||||
<Slider
|
||||
value={intensite}
|
||||
onChange={setIntensite}
|
||||
min={1}
|
||||
max={10}
|
||||
label="Intensité de l'émotion"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||||
<span>Faible</span>
|
||||
<span>Modérée</span>
|
||||
<span>Forte</span>
|
||||
</div>
|
||||
{/* Intensité */}
|
||||
<div className="fr-input-group">
|
||||
<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-1v">
|
||||
<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>
|
||||
|
||||
{/* Date */}
|
||||
<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>
|
||||
{/* Date */}
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="edit-saisie-date">
|
||||
Date de la saisie
|
||||
</label>
|
||||
<input
|
||||
id="edit-saisie-date"
|
||||
className="fr-input"
|
||||
type="date"
|
||||
value={dateSaisie}
|
||||
onChange={(e) => setDateSaisie(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<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..."
|
||||
rows={3}
|
||||
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>
|
||||
{/* Note */}
|
||||
<div className="fr-input-group">
|
||||
<label className="fr-label" htmlFor="edit-saisie-note">
|
||||
Note (optionnel)
|
||||
</label>
|
||||
<textarea
|
||||
id="edit-saisie-note"
|
||||
className="fr-input"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder="Décrivez ce que vous ressentez..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
{/* Actions */}
|
||||
<ul className="fr-btns-group fr-btns-group--inline-md fr-btns-group--right">
|
||||
<li>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Annuler
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={updateSaisie.isPending}
|
||||
@@ -121,8 +127,8 @@ export default function EditSaisieModal({ saisie, isOpen, onClose }: EditSaisieM
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
LineChart,
|
||||
Line,
|
||||
Legend,
|
||||
} from 'recharts';
|
||||
import type { ValueType, NameType } from 'recharts/types/component/DefaultTooltipContent';
|
||||
@@ -38,7 +36,7 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-80">
|
||||
<div style={{ width: '100%', height: '20rem' }}>
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
@@ -88,12 +86,12 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-80">
|
||||
<div style={{ width: '100%', height: '20rem' }}>
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<LineChart data={lineData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
||||
<BarChart data={lineData}>
|
||||
<CartesianGrid strokeDasharray="3 3" style={{ opacity: 0.3 }} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
||||
<YAxis tick={{ fontSize: 12 }} />
|
||||
<YAxis tick={{ fontSize: 12 }} allowDecimals={false} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||
@@ -104,17 +102,14 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
/>
|
||||
<Legend />
|
||||
{data.evolution.emotions.map((emotion) => (
|
||||
<Line
|
||||
<Bar
|
||||
key={emotion.nom}
|
||||
type="monotone"
|
||||
dataKey={emotion.nom}
|
||||
stroke={emotion.couleur}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: emotion.couleur, r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
stackId="emotions"
|
||||
fill={emotion.couleur}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
@@ -131,10 +126,10 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-80">
|
||||
<div style={{ width: '100%', height: '20rem' }}>
|
||||
<ResponsiveContainer width="100%" height={320}>
|
||||
<BarChart data={barData}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="opacity-30" />
|
||||
<CartesianGrid strokeDasharray="3 3" style={{ opacity: 0.3 }} />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 12 }} />
|
||||
<YAxis domain={[0, 10]} tick={{ fontSize: 12 }} />
|
||||
<Tooltip
|
||||
@@ -146,7 +141,7 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
}}
|
||||
formatter={(value: ValueType | undefined) => [`${Number(value).toFixed(1)}`, 'Intensité moyenne']}
|
||||
/>
|
||||
<Bar dataKey="intensite" fill="#fce117" radius={[6, 6, 0, 0]} />
|
||||
<Bar dataKey="intensite" fill="#000091" radius={[6, 6, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -158,8 +153,17 @@ export default function EmotionChart({ data, type = 'repartition' }: EmotionChar
|
||||
|
||||
function EmptyChart({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="w-full h-80 flex items-center justify-center text-gray-400 dark:text-gray-600">
|
||||
<p className="text-lg">{message}</p>
|
||||
<div
|
||||
className="fr-text-mention--grey"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '20rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<p className="fr-text--lead fr-mb-0">{message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,7 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
|
||||
const [selectedParent, setSelectedParent] = useState<Emotion | null>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 7 }).map((_, i) => (
|
||||
<div key={i} className="h-20 rounded-xl bg-gray-100 dark:bg-gray-800 animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
return <p className="fr-text--sm fr-text-mention--grey">Chargement des émotions…</p>;
|
||||
}
|
||||
|
||||
// Si une émotion parent est sélectionnée et qu'elle a des enfants, afficher les sous-émotions
|
||||
@@ -30,76 +24,75 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => 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"
|
||||
className="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-icon-arrow-left-line fr-btn--icon-left fr-mb-2w"
|
||||
>
|
||||
← Retour aux émotions
|
||||
Retour aux émotions
|
||||
</button>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||
Précisez votre émotion de <span className="font-medium" style={{ color: selectedParent.couleur }}>{selectedParent.nom}</span> :
|
||||
<p className="fr-text--sm fr-mb-2w">
|
||||
Précisez votre émotion de{' '}
|
||||
<span className="fr-text--bold" style={{ color: selectedParent.couleur }}>
|
||||
{selectedParent.nom}
|
||||
</span>{' '}
|
||||
:
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{selectedParent.enfants.map((child) => (
|
||||
<button
|
||||
key={child.id}
|
||||
type="button"
|
||||
onClick={() => 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,
|
||||
}}
|
||||
>
|
||||
<span className="text-2xl">{child.icone || '🔵'}</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{child.nom}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<ul className="fr-tags-group">
|
||||
{selectedParent.enfants.map((child) => {
|
||||
const isSelected = value === child.id;
|
||||
return (
|
||||
<li key={child.id}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => onChange(child.id)}
|
||||
className={`fr-tag${isSelected ? ' fr-tag--green-emeraude' : ''}`}
|
||||
>
|
||||
<span aria-hidden="true" className="fr-mr-1v">
|
||||
{child.icone || '🔵'}
|
||||
</span>
|
||||
{child.nom}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
<ul className="fr-tags-group">
|
||||
{emotions?.map((emotion) => {
|
||||
const hasChildren = emotion.enfants && emotion.enfants.length > 0;
|
||||
const isSelected = value === emotion.id || emotion.enfants?.some(e => e.id === value);
|
||||
const isSelected =
|
||||
value === emotion.id || emotion.enfants?.some((e) => e.id === value);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={emotion.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<span className="text-3xl">{emotion.icone || '🔵'}</span>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{emotion.nom}</span>
|
||||
{hasChildren && (
|
||||
<span className="text-xs text-gray-400">▼</span>
|
||||
)}
|
||||
</button>
|
||||
<li key={emotion.id}>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={!!isSelected}
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
setSelectedParent(emotion);
|
||||
} else {
|
||||
onChange(emotion.id);
|
||||
}
|
||||
}}
|
||||
className={`fr-tag fr-tag--md${isSelected ? ' fr-tag--green-emeraude' : ''}`}
|
||||
>
|
||||
<span aria-hidden="true" className="fr-mr-1v">
|
||||
{emotion.icone || '🔵'}
|
||||
</span>
|
||||
{emotion.nom}
|
||||
{hasChildren && (
|
||||
<span aria-hidden="true" className="fr-ml-1v">
|
||||
▾
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
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[];
|
||||
@@ -14,9 +12,11 @@ interface TrackerTimelineProps {
|
||||
export default function TrackerTimeline({ saisies, onEdit, onDelete }: TrackerTimelineProps) {
|
||||
if (saisies.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12 text-gray-400 dark:text-gray-600">
|
||||
<p className="text-lg mb-2">Aucune saisie enregistrée</p>
|
||||
<p className="text-sm">Commencez par enregistrer votre première émotion</p>
|
||||
<div className="fr-callout">
|
||||
<p className="fr-callout__title">Aucune saisie enregistrée</p>
|
||||
<p className="fr-callout__text">
|
||||
Commencez par enregistrer votre première émotion.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,78 +35,105 @@ export default function TrackerTimeline({ saisies, onEdit, onDelete }: TrackerTi
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{Object.entries(grouped).map(([date, items]) => (
|
||||
<div key={date}>
|
||||
<h3 className="text-sm font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-4">
|
||||
<section key={date} className="fr-mb-4w">
|
||||
<h3 className="fr-h6 fr-text-mention--grey fr-mb-2w" style={{ textTransform: 'uppercase' }}>
|
||||
{date}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="fr-grid-row fr-grid-row--gutters">
|
||||
{items.map((saisie) => (
|
||||
<div
|
||||
key={saisie.id}
|
||||
className="flex items-start gap-4 p-4 bg-white dark:bg-gray-900 rounded-xl border border-gray-100 dark:border-gray-800 hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
{/* Icône émotion */}
|
||||
<div
|
||||
className="shrink-0 w-12 h-12 rounded-xl flex items-center justify-center text-xl"
|
||||
style={{ backgroundColor: `${saisie.emotion?.couleur}20` }}
|
||||
>
|
||||
{saisie.emotion?.icone || '🔵'}
|
||||
</div>
|
||||
<div key={saisie.id} className="fr-col-12">
|
||||
<div className="app-panel fr-p-2w">
|
||||
<div className="fr-grid-row fr-grid-row--gutters fr-grid-row--top">
|
||||
{/* Icône émotion */}
|
||||
<div className="fr-col-auto">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '3rem',
|
||||
height: '3rem',
|
||||
borderRadius: '0.5rem',
|
||||
fontSize: '1.5rem',
|
||||
backgroundColor: `${saisie.emotion?.couleur}20`,
|
||||
}}
|
||||
>
|
||||
{saisie.emotion?.icone || '🔵'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Contenu */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge color={saisie.emotion?.couleur}>
|
||||
{saisie.emotion?.nom}
|
||||
</Badge>
|
||||
<span className="text-sm text-gray-400 dark:text-gray-500">
|
||||
Intensité : {saisie.intensite}/10
|
||||
</span>
|
||||
</div>
|
||||
{/* Barre d'intensité */}
|
||||
<div className="w-full h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full mb-2">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${saisie.intensite * 10}%`,
|
||||
backgroundColor: saisie.emotion?.couleur || '#fce117',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{saisie.note && (
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 line-clamp-2">
|
||||
{saisie.note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Contenu */}
|
||||
<div className="fr-col">
|
||||
<p className="fr-mb-1v">
|
||||
<span className="fr-badge fr-badge--info fr-mr-2v">
|
||||
{saisie.emotion?.nom}
|
||||
</span>
|
||||
<span className="fr-text--sm fr-text-mention--grey">
|
||||
Intensité : {saisie.intensite}/10
|
||||
</span>
|
||||
</p>
|
||||
{/* Barre d'intensité */}
|
||||
<div
|
||||
className="fr-mb-1w"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '0.375rem',
|
||||
borderRadius: '0.25rem',
|
||||
backgroundColor: 'var(--background-contrast-grey)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${saisie.intensite * 10}%`,
|
||||
backgroundColor: saisie.emotion?.couleur || '#000091',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{saisie.note && (
|
||||
<p className="fr-text--sm fr-mb-0">{saisie.note}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
)}
|
||||
{/* Actions */}
|
||||
<div className="fr-col-auto">
|
||||
<ul className="fr-btns-group fr-btns-group--inline fr-btns-group--sm">
|
||||
{onEdit && (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit(saisie)}
|
||||
className="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-icon-edit-line"
|
||||
title="Modifier"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
{onDelete && (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onDelete(saisie.id)}
|
||||
className="fr-btn fr-btn--tertiary-no-outline fr-btn--sm fr-icon-delete-line"
|
||||
title="Supprimer"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,60 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { Phone } from "lucide-react";
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-gray-50 dark:bg-gray-950 border-t border-gray-200 dark:border-gray-800">
|
||||
{/* Bandeau urgence */}
|
||||
<div className="bg-red-50 dark:bg-red-950/50 border-b border-red-100 dark:border-red-900">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
|
||||
<p className="flex items-center justify-center gap-2 text-sm text-red-700 dark:text-red-400 font-medium text-center">
|
||||
<Phone size={16} className="shrink-0" />
|
||||
En cas d'urgence : 3114 (numéro national de prévention du suicide)
|
||||
</p>
|
||||
<footer className="fr-footer" role="contentinfo" id="footer">
|
||||
<div className="fr-container">
|
||||
<div className="fr-footer__body">
|
||||
<div className="fr-footer__brand fr-enlarge-link">
|
||||
<Link href="/" title="Accueil - CESIZen">
|
||||
<p className="fr-logo">
|
||||
République
|
||||
<br />
|
||||
Française
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="fr-footer__content">
|
||||
<p className="fr-footer__content-desc">
|
||||
CESIZen est une plateforme publique de prévention et de suivi en
|
||||
santé mentale, éditée pour le Ministère des Solidarités et de la
|
||||
Santé. En cas de détresse, contactez le 3114 (numéro national de
|
||||
prévention du suicide).
|
||||
</p>
|
||||
<ul className="fr-footer__content-list">
|
||||
<li className="fr-footer__content-item">
|
||||
<a
|
||||
className="fr-footer__content-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
href="https://legifrance.gouv.fr"
|
||||
>
|
||||
legifrance.gouv.fr
|
||||
</a>
|
||||
</li>
|
||||
<li className="fr-footer__content-item">
|
||||
<a
|
||||
className="fr-footer__content-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
href="https://gouvernement.fr"
|
||||
>
|
||||
gouvernement.fr
|
||||
</a>
|
||||
</li>
|
||||
<li className="fr-footer__content-item">
|
||||
<a
|
||||
className="fr-footer__content-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
href="https://service-public.fr"
|
||||
>
|
||||
service-public.fr
|
||||
</a>
|
||||
</li>
|
||||
<li className="fr-footer__content-item">
|
||||
<a
|
||||
className="fr-footer__content-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
href="https://data.gouv.fr"
|
||||
>
|
||||
data.gouv.fr
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
{/* Logo + Copyright */}
|
||||
<div className="flex flex-col items-center md:items-start gap-2">
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-xl font-bold font-display text-malachite-500">
|
||||
CESI
|
||||
</span>
|
||||
<span className="text-xl font-bold font-display text-candlelight-500">
|
||||
ZEN
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 text-center md:text-left">
|
||||
CESIZen 2024 - Ministère des Solidarités et de la Santé
|
||||
<div className="fr-footer__bottom">
|
||||
<ul className="fr-footer__bottom-list">
|
||||
<li className="fr-footer__bottom-item">
|
||||
<Link className="fr-footer__bottom-link" href="/mentions-legales">
|
||||
Mentions légales
|
||||
</Link>
|
||||
</li>
|
||||
<li className="fr-footer__bottom-item">
|
||||
<Link
|
||||
className="fr-footer__bottom-link"
|
||||
href="/politique-de-confidentialite"
|
||||
>
|
||||
Données personnelles
|
||||
</Link>
|
||||
</li>
|
||||
<li className="fr-footer__bottom-item">
|
||||
<Link className="fr-footer__bottom-link" href="/contacts-urgence">
|
||||
Contacts d'urgence
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="fr-footer__bottom-copy">
|
||||
<p>
|
||||
Sauf mention contraire, tous les contenus de ce site sont sous{" "}
|
||||
<a
|
||||
href="https://github.com/etalab/licence-ouverte/blob/master/LO.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer external"
|
||||
>
|
||||
licence etalab-2.0
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Links */}
|
||||
<nav className="flex flex-wrap items-center justify-center gap-6">
|
||||
<Link
|
||||
href="/mentions-legales"
|
||||
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||
>
|
||||
Mentions légales
|
||||
</Link>
|
||||
<Link
|
||||
href="/politique-de-confidentialite"
|
||||
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||
>
|
||||
Politique de confidentialité
|
||||
</Link>
|
||||
<Link
|
||||
href="/contacts-urgence"
|
||||
className="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||
>
|
||||
Contacts d'urgence
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,245 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { 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 { usePathname, useRouter } from "next/navigation";
|
||||
import { useAuthStore } from "@/lib/auth-store";
|
||||
import ConfirmDialog from "@/components/ui/ConfirmDialog";
|
||||
|
||||
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 router = useRouter();
|
||||
const { user, token, logout } = useAuthStore();
|
||||
const isAuthenticated = !!token;
|
||||
const isAdmin = user?.role?.nom === "administrateur";
|
||||
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
|
||||
|
||||
const publicLinks: NavLink[] = [
|
||||
{ href: "/", label: "Accueil", icon: <Home size={18} /> },
|
||||
{ href: "/informations", label: "Informations", icon: <BookOpen size={18} /> },
|
||||
];
|
||||
|
||||
const authLinks: NavLink[] = [
|
||||
{ href: "/journal", label: "Journal", icon: <BookOpen size={18} /> },
|
||||
{ href: "/journal/rapports", label: "Rapports", icon: <BarChart3 size={18} /> },
|
||||
];
|
||||
|
||||
const adminLinks: NavLink[] = [
|
||||
{ href: "/admin", label: "Admin", icon: <Settings size={18} /> },
|
||||
];
|
||||
|
||||
const navLinks = [
|
||||
...publicLinks,
|
||||
...(isAuthenticated ? authLinks : []),
|
||||
...(isAdmin ? adminLinks : []),
|
||||
];
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === "/") return pathname === "/";
|
||||
return pathname.startsWith(href);
|
||||
const handleLogout = async () => {
|
||||
setShowLogoutConfirm(false);
|
||||
await logout();
|
||||
router.push("/connexion");
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setUserDropdownOpen(false);
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
const navLinks: NavLink[] = [
|
||||
{ href: "/", label: "Accueil" },
|
||||
{ href: "/informations", label: "Informations" },
|
||||
{ href: "/contacts-urgence", label: "Contacts d'urgence" },
|
||||
...(isAuthenticated
|
||||
? [
|
||||
{ href: "/journal", label: "Mon journal" },
|
||||
{ href: "/journal/rapports", label: "Mes rapports" },
|
||||
]
|
||||
: []),
|
||||
...(isAdmin ? [{ href: "/admin/dashboard", label: "Administration" }] : []),
|
||||
];
|
||||
|
||||
const isActive = (href: string) =>
|
||||
href === "/" ? pathname === "/" : pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-lg border-b border-gray-200 dark:border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-0.5 shrink-0">
|
||||
<span className="text-2xl font-bold font-display text-malachite-500">
|
||||
CESI
|
||||
</span>
|
||||
<span className="text-2xl font-bold font-display text-candlelight-500">
|
||||
ZEN
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className={`
|
||||
flex items-center gap-2 px-4 py-2 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 hover:text-gray-900 dark:hover:text-gray-200"
|
||||
}
|
||||
`}
|
||||
>
|
||||
{link.icon}
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Right actions */}
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
|
||||
{/* Desktop auth */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
{isAuthenticated ? (
|
||||
<div className="relative">
|
||||
<header role="banner" className="fr-header">
|
||||
<div className="fr-header__body">
|
||||
<div className="fr-container">
|
||||
<div className="fr-header__body-row">
|
||||
<div className="fr-header__brand fr-enlarge-link">
|
||||
<div className="fr-header__brand-top">
|
||||
<div className="fr-header__logo">
|
||||
<p className="fr-logo">
|
||||
République
|
||||
<br />
|
||||
Française
|
||||
</p>
|
||||
</div>
|
||||
<div className="fr-header__navbar">
|
||||
<button
|
||||
onClick={() => 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"
|
||||
className="fr-btn--menu fr-btn"
|
||||
data-fr-opened="false"
|
||||
aria-controls="modal-menu-mobile"
|
||||
aria-haspopup="menu"
|
||||
id="button-menu-mobile"
|
||||
title="Menu"
|
||||
>
|
||||
<div className="h-8 w-8 rounded-full bg-malachite-100 dark:bg-malachite-900 flex items-center justify-center">
|
||||
<User
|
||||
size={16}
|
||||
className="text-malachite-600 dark:text-malachite-400"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{user?.prenom || "Utilisateur"}
|
||||
</span>
|
||||
Menu
|
||||
</button>
|
||||
|
||||
{userDropdownOpen && (
|
||||
</div>
|
||||
</div>
|
||||
<div className="fr-header__service">
|
||||
<Link href="/" title="Accueil - CESIZen">
|
||||
<span className="fr-header__service-title">CESIZen</span>
|
||||
</Link>
|
||||
<p className="fr-header__service-tagline">
|
||||
Votre santé mentale — Ministère des Solidarités et de la Santé
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fr-header__tools">
|
||||
<div className="fr-header__tools-links">
|
||||
<ul className="fr-btns-group">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setUserDropdownOpen(false)}
|
||||
/>
|
||||
<div className="absolute right-0 mt-2 w-48 z-20 bg-white dark:bg-gray-900 rounded-xl shadow-lg border border-gray-100 dark:border-gray-800 py-1 animate-in fade-in zoom-in-95 duration-150">
|
||||
<li>
|
||||
<Link
|
||||
className="fr-btn fr-icon-account-line"
|
||||
href="/profil"
|
||||
onClick={() => 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"
|
||||
>
|
||||
<Settings size={16} />
|
||||
Mon profil
|
||||
{user?.prenom || "Mon profil"}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-2 px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 transition-colors"
|
||||
className="fr-btn fr-icon-logout-box-r-line"
|
||||
onClick={() => setShowLogoutConfirm(true)}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
<Link
|
||||
className="fr-btn fr-icon-account-line"
|
||||
href="/connexion"
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="fr-btn fr-icon-add-line"
|
||||
href="/inscription"
|
||||
>
|
||||
Créer un compte
|
||||
</Link>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/connexion"
|
||||
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-xl transition-colors"
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
href="/inscription"
|
||||
className="px-4 py-2 text-sm font-semibold bg-candlelight-500 hover:bg-candlelight-600 text-black rounded-xl transition-all duration-200 shadow-sm hover:shadow-md"
|
||||
>
|
||||
Inscription
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
onClick={() => 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 ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-950 animate-in slide-in-from-top duration-200">
|
||||
<nav className="px-4 py-4 space-y-1">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => 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}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="pt-4 border-t border-gray-200 dark:border-gray-800 mt-4 space-y-1">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
{/* Navigation principale (et menu mobile DSFR) */}
|
||||
<div
|
||||
className="fr-header__menu fr-modal"
|
||||
id="modal-menu-mobile"
|
||||
aria-labelledby="button-menu-mobile"
|
||||
>
|
||||
<div className="fr-container">
|
||||
<button
|
||||
className="fr-btn--close fr-btn"
|
||||
aria-controls="modal-menu-mobile"
|
||||
title="Fermer"
|
||||
>
|
||||
Fermer
|
||||
</button>
|
||||
{/* Conteneur requis par le JS DSFR (recopie des liens d'action en mobile) */}
|
||||
<div className="fr-header__menu-links"></div>
|
||||
<nav
|
||||
className="fr-nav"
|
||||
id="navigation-principale"
|
||||
role="navigation"
|
||||
aria-label="Menu principal"
|
||||
>
|
||||
<ul className="fr-nav__list">
|
||||
{navLinks.map((link) => (
|
||||
<li className="fr-nav__item" key={link.href}>
|
||||
<Link
|
||||
href="/profil"
|
||||
onClick={() => 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"
|
||||
className="fr-nav__link"
|
||||
href={link.href}
|
||||
target="_self"
|
||||
aria-current={isActive(link.href) ? "page" : undefined}
|
||||
>
|
||||
<User size={18} />
|
||||
Mon profil
|
||||
{link.label}
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950 transition-colors"
|
||||
>
|
||||
<LogOut size={18} />
|
||||
Déconnexion
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/connexion"
|
||||
onClick={() => 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
|
||||
</Link>
|
||||
<Link
|
||||
href="/inscription"
|
||||
onClick={() => 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
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
isOpen={showLogoutConfirm}
|
||||
title="Déconnexion"
|
||||
message="Voulez-vous vraiment vous déconnecter de votre compte CESIZen ?"
|
||||
confirmLabel="Se déconnecter"
|
||||
cancelLabel="Annuler"
|
||||
onConfirm={handleLogout}
|
||||
onClose={() => setShowLogoutConfirm(false)}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,41 +3,33 @@
|
||||
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;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const sidebarLinks: SidebarLink[] = [
|
||||
{
|
||||
href: "/admin",
|
||||
label: "Dashboard",
|
||||
icon: <LayoutDashboard size={20} />,
|
||||
icon: "fr-icon-dashboard-3-line",
|
||||
},
|
||||
{
|
||||
href: "/admin/utilisateurs",
|
||||
label: "Utilisateurs",
|
||||
icon: <Users size={20} />,
|
||||
icon: "fr-icon-user-line",
|
||||
},
|
||||
{
|
||||
href: "/admin/contenus",
|
||||
label: "Contenus",
|
||||
icon: <FileText size={20} />,
|
||||
icon: "fr-icon-file-text-line",
|
||||
},
|
||||
{
|
||||
href: "/admin/emotions",
|
||||
label: "Émotions",
|
||||
icon: <Heart size={20} />,
|
||||
icon: "fr-icon-heart-line",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -51,83 +43,49 @@ export default function Sidebar() {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile toggle */}
|
||||
<button
|
||||
onClick={() => 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 ? <X size={20} /> : <Menu size={20} />}
|
||||
</button>
|
||||
|
||||
{/* Overlay mobile */}
|
||||
{collapsed && (
|
||||
<nav
|
||||
className="fr-sidemenu"
|
||||
aria-labelledby="fr-sidemenu-title"
|
||||
role="navigation"
|
||||
>
|
||||
<div className="fr-sidemenu__inner">
|
||||
<button
|
||||
type="button"
|
||||
className="fr-sidemenu__btn"
|
||||
aria-controls="fr-sidemenu-wrapper"
|
||||
aria-expanded={collapsed}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
Menu d'administration
|
||||
</button>
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 z-30 bg-black/50 backdrop-blur-sm"
|
||||
onClick={() => setCollapsed(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`
|
||||
fixed lg:sticky top-0 left-0 z-40 lg:z-auto
|
||||
h-screen w-64
|
||||
bg-white dark:bg-gray-950
|
||||
border-r border-gray-200 dark:border-gray-800
|
||||
flex flex-col
|
||||
transition-transform duration-300 ease-in-out
|
||||
${collapsed ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}
|
||||
`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center justify-between h-16 px-6 border-b border-gray-200 dark:border-gray-800">
|
||||
<Link href="/admin" className="flex items-center gap-0.5">
|
||||
<span className="text-xl font-bold font-display text-malachite-500">
|
||||
CESI
|
||||
</span>
|
||||
<span className="text-xl font-bold font-display text-candlelight-500">
|
||||
ZEN
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setCollapsed(false)}
|
||||
className="lg:hidden p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
|
||||
{sidebarLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
onClick={() => 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}
|
||||
className={`fr-collapse${collapsed ? " fr-collapse--expanded" : ""}`}
|
||||
id="fr-sidemenu-wrapper"
|
||||
>
|
||||
<div className="fr-sidemenu__title" id="fr-sidemenu-title">
|
||||
<Link href="/admin" className="fr-sidemenu__link">
|
||||
Administration CESIZen
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-800">
|
||||
<p className="text-xs text-gray-400 dark:text-gray-600">
|
||||
Administration CESIZen
|
||||
</p>
|
||||
</div>
|
||||
<ul className="fr-sidemenu__list">
|
||||
{sidebarLinks.map((link) => {
|
||||
const active = isActive(link.href);
|
||||
return (
|
||||
<li className="fr-sidemenu__item" key={link.href}>
|
||||
<Link
|
||||
className={`fr-sidemenu__link ${link.icon} fr-link--icon-left`}
|
||||
href={link.href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,27 +8,41 @@ interface BadgeProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Mappe une couleur (hex) vers une variante de statut DSFR `fr-badge--*`.
|
||||
// Conserve la prop `color` pour garder la même signature, mais le rendu
|
||||
// utilise désormais le système de couleurs du DSFR (thème clair/sombre géré).
|
||||
function badgeVariant(color: string): string {
|
||||
const c = color.toLowerCase();
|
||||
switch (c) {
|
||||
case "#22c55e":
|
||||
case "#16a34a":
|
||||
case "green":
|
||||
return "fr-badge--success";
|
||||
case "#ef4444":
|
||||
case "#dc2626":
|
||||
case "red":
|
||||
return "fr-badge--error";
|
||||
case "#f59e0b":
|
||||
case "#fce117":
|
||||
case "orange":
|
||||
return "fr-badge--warning";
|
||||
case "#3b82f6":
|
||||
case "#0ea5e9":
|
||||
case "blue":
|
||||
return "fr-badge--info";
|
||||
default:
|
||||
return "fr-badge--new";
|
||||
}
|
||||
}
|
||||
|
||||
export default function Badge({
|
||||
children,
|
||||
color = "#fce117",
|
||||
className = "",
|
||||
}: BadgeProps) {
|
||||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center
|
||||
px-2.5 py-0.5
|
||||
rounded-full
|
||||
text-xs font-medium
|
||||
transition-all duration-200
|
||||
${className}
|
||||
`}
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
const classes = ["fr-badge", "fr-badge--sm", badgeVariant(color), className]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return <span className={classes}>{children}</span>;
|
||||
}
|
||||
|
||||
@@ -11,23 +11,19 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
// Mappe les variantes internes sur les classes du Système de Design de l'État (DSFR).
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
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",
|
||||
primary: "",
|
||||
secondary: "fr-btn--secondary",
|
||||
outline: "fr-btn--secondary",
|
||||
ghost: "fr-btn--tertiary-no-outline",
|
||||
danger: "fr-btn--secondary",
|
||||
};
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: "px-3 py-1.5 text-sm",
|
||||
md: "px-5 py-2.5 text-base",
|
||||
lg: "px-7 py-3.5 text-lg",
|
||||
sm: "fr-btn--sm",
|
||||
md: "",
|
||||
lg: "fr-btn--lg",
|
||||
};
|
||||
|
||||
export default function Button({
|
||||
@@ -39,43 +35,18 @@ export default function Button({
|
||||
className = "",
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const classes = [
|
||||
"fr-btn",
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
loading ? "fr-btn--icon-left fr-icon-refresh-line" : "",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<button
|
||||
disabled={disabled || loading}
|
||||
className={`
|
||||
inline-flex items-center justify-center gap-2
|
||||
rounded-xl transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:ring-offset-2
|
||||
dark:focus:ring-offset-gray-900
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${variantClasses[variant]}
|
||||
${sizeClasses[size]}
|
||||
${className}
|
||||
`}
|
||||
{...rest}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="animate-spin h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<button className={classes} disabled={disabled || loading} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -8,20 +8,17 @@ interface CardProps {
|
||||
hover?: boolean;
|
||||
}
|
||||
|
||||
// Conteneur neutre au style DSFR (fr-card sans la structure imposée d'une carte
|
||||
// éditoriale DSFR, afin de garder un conteneur générique réutilisable).
|
||||
export default function Card({ children, className = "", hover = false }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
bg-white dark:bg-gray-900
|
||||
rounded-2xl shadow-sm
|
||||
border border-gray-100 dark:border-gray-800
|
||||
p-6
|
||||
transition-all duration-200
|
||||
${hover ? "hover:shadow-lg hover:-translate-y-0.5" : ""}
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const classes = [
|
||||
"app-panel",
|
||||
"fr-p-3w",
|
||||
hover ? "fr-enlarge-link" : "",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return <div className={classes}>{children}</div>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
|
||||
@@ -27,42 +26,26 @@ export default function ConfirmDialog({
|
||||
}: ConfirmDialogProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={title} size="sm">
|
||||
<div className="flex items-start gap-3 mb-6">
|
||||
<div
|
||||
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
destructive
|
||||
? "bg-red-50 dark:bg-red-950/40"
|
||||
: "bg-malachite-50 dark:bg-malachite-950/40"
|
||||
}`}
|
||||
>
|
||||
<AlertTriangle
|
||||
size={20}
|
||||
className={
|
||||
destructive
|
||||
? "text-red-600 dark:text-red-400"
|
||||
: "text-malachite-600 dark:text-malachite-400"
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 leading-relaxed pt-1">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
<p className="fr-mb-3w">{message}</p>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
<Button
|
||||
variant={destructive ? "danger" : "primary"}
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="fr-btns-group fr-btns-group--right fr-btns-group--inline-reverse fr-btns-group--inline-lg">
|
||||
<li>
|
||||
<Button
|
||||
variant={destructive ? "danger" : "primary"}
|
||||
onClick={() => {
|
||||
onConfirm();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{confirmLabel}
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
{cancelLabel}
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,42 +17,26 @@ export default function Input({
|
||||
...inputProps
|
||||
}: InputProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
|
||||
const errorId = error ? `${inputId}-error` : undefined;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className={`fr-input-group${error ? " fr-input-group--error" : ""}`}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
|
||||
>
|
||||
<label className="fr-label" htmlFor={inputId}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className="relative">
|
||||
{icon && (
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 dark:text-gray-500">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<div className={icon ? "fr-input-wrap" : undefined}>
|
||||
{icon && <span aria-hidden="true">{icon}</span>}
|
||||
<input
|
||||
id={inputId}
|
||||
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
|
||||
placeholder:text-gray-400 dark:placeholder:text-gray-500
|
||||
focus:outline-none focus:ring-2 focus:ring-candlelight-500 focus:border-candlelight-500
|
||||
transition-all duration-200
|
||||
px-4 py-2.5 text-base
|
||||
${icon ? "pl-10" : ""}
|
||||
${error ? "border-red-500 focus:ring-red-500 focus:border-red-500" : ""}
|
||||
${className}
|
||||
`}
|
||||
className={`fr-input${error ? " fr-input--error" : ""} ${className}`.trim()}
|
||||
aria-describedby={errorId}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
{error && (
|
||||
<p className="mt-1.5 text-sm text-red-500 dark:text-red-400">
|
||||
<p id={errorId} className="fr-error-text">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
type ModalSize = "sm" | "md" | "lg";
|
||||
|
||||
@@ -13,10 +12,10 @@ interface ModalProps {
|
||||
size?: ModalSize;
|
||||
}
|
||||
|
||||
const sizeClasses: Record<ModalSize, string> = {
|
||||
sm: "max-w-sm",
|
||||
md: "max-w-lg",
|
||||
lg: "max-w-2xl",
|
||||
const maxWidthBySize: Record<ModalSize, string> = {
|
||||
sm: "480px",
|
||||
md: "672px",
|
||||
lg: "896px",
|
||||
};
|
||||
|
||||
export default function Modal({
|
||||
@@ -34,10 +33,9 @@ export default function Modal({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
if (!isOpen) return;
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
document.body.style.overflow = "";
|
||||
@@ -46,43 +44,51 @@ export default function Modal({
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Overlay React maîtrisé (pas le JS DSFR) : fond assombri visible, fermeture
|
||||
// au clic extérieur / Échap / bouton. Look DSFR via les classes fr-modal__*.
|
||||
return (
|
||||
<div className="fixed inset-0 z-100 flex items-center justify-center p-4">
|
||||
{/* Overlay */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "center",
|
||||
overflowY: "auto",
|
||||
padding: "2rem 1rem",
|
||||
background: "rgba(22, 22, 22, 0.64)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm animate-in fade-in duration-200"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div
|
||||
className={`
|
||||
relative w-full ${sizeClasses[size]}
|
||||
bg-white dark:bg-gray-900
|
||||
rounded-2xl shadow-2xl
|
||||
border border-gray-100 dark:border-gray-800
|
||||
p-6
|
||||
animate-in zoom-in-95 fade-in duration-200
|
||||
`}
|
||||
className="fr-modal__body"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: maxWidthBySize[size],
|
||||
background: "var(--background-default-grey, #fff)",
|
||||
borderRadius: "0.25rem",
|
||||
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.32)",
|
||||
padding: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
{title && (
|
||||
<h2 className="text-xl font-bold font-display text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div className="fr-modal__header">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="ml-auto p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-gray-500 dark:text-gray-400"
|
||||
type="button"
|
||||
className="fr-btn--close fr-btn"
|
||||
aria-label="Fermer"
|
||||
onClick={onClose}
|
||||
>
|
||||
<X size={20} />
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>{children}</div>
|
||||
<div className="fr-modal__content">
|
||||
{title && <h1 className="fr-modal__title">{title}</h1>}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,10 +9,12 @@ interface SkeletonProps {
|
||||
variant?: SkeletonVariant;
|
||||
}
|
||||
|
||||
const variantClasses: Record<SkeletonVariant, string> = {
|
||||
text: "h-4 w-full rounded",
|
||||
circle: "h-10 w-10 rounded-full",
|
||||
rect: "h-24 w-full rounded-xl",
|
||||
// Dimensions/forme selon la variante. Couleur neutre gérée en ligne pour
|
||||
// rester cohérent avec le thème DSFR (sans couleur custom Tailwind).
|
||||
const variantStyle: Record<SkeletonVariant, React.CSSProperties> = {
|
||||
text: { height: "1rem", width: "100%", borderRadius: "0.25rem" },
|
||||
circle: { height: "2.5rem", width: "2.5rem", borderRadius: "9999px" },
|
||||
rect: { height: "6rem", width: "100%", borderRadius: "0.25rem" },
|
||||
};
|
||||
|
||||
export default function Skeleton({
|
||||
@@ -21,12 +23,14 @@ export default function Skeleton({
|
||||
}: SkeletonProps) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
animate-pulse
|
||||
bg-gray-200 dark:bg-gray-700
|
||||
${variantClasses[variant]}
|
||||
${className}
|
||||
`}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
...variantStyle[variant],
|
||||
backgroundColor: "var(--background-contrast-grey)",
|
||||
opacity: 0.6,
|
||||
animation: "fr-skeleton-pulse 1.5s ease-in-out infinite",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,33 +19,45 @@ export default function Slider({
|
||||
label,
|
||||
showValue = true,
|
||||
}: SliderProps) {
|
||||
const rangeId = label
|
||||
? `range-${label.toLowerCase().replace(/\s+/g, "-")}`
|
||||
: "range";
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="fr-range-group">
|
||||
{(label || showValue) && (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{label && (
|
||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<label className="fr-label" htmlFor={rangeId}>
|
||||
{label}
|
||||
{showValue && (
|
||||
<span className="text-sm font-bold text-candlelight-600 dark:text-candlelight-400 bg-candlelight-50 dark:bg-candlelight-950 px-2 py-0.5 rounded-lg">
|
||||
{value}
|
||||
</span>
|
||||
<span className="fr-hint-text">Valeur sélectionnée : {value}</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{min}</span>
|
||||
<span className="text-xs text-gray-400 dark:text-gray-500">{max}</span>
|
||||
<div
|
||||
className="fr-range fr-range--sm"
|
||||
data-fr-js-range="true"
|
||||
data-fr-min={min}
|
||||
data-fr-max={max}
|
||||
>
|
||||
<span
|
||||
className="fr-range__output"
|
||||
aria-hidden="true"
|
||||
>{`${value}`}</span>
|
||||
<input
|
||||
id={rangeId}
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
step={1}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
<div className="fr-range__min" aria-hidden="true">
|
||||
{min}
|
||||
</div>
|
||||
<div className="fr-range__max" aria-hidden="true">
|
||||
{max}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,35 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Sun, Moon } from "lucide-react";
|
||||
|
||||
type Scheme = "light" | "dark";
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const { theme, setTheme } = useTheme();
|
||||
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
|
||||
className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||
type="button"
|
||||
className="fr-btn fr-btn--tertiary fr-btn--icon-left fr-icon-theme-fill"
|
||||
aria-label="Changer le thème"
|
||||
>
|
||||
<div className="h-5 w-5" />
|
||||
Thème
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => 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"}
|
||||
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"}
|
||||
>
|
||||
{theme === "dark" ? <Sun size={20} /> : <Moon size={20} />}
|
||||
{scheme === "dark" ? "Mode clair" : "Mode sombre"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ export default function TabLayout() {
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Accueil',
|
||||
headerShown: false,
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
+18
-13
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/lib/auth-store';
|
||||
import { useRapport } from '@/hooks/useRapport';
|
||||
import { useSaisies } from '@/hooks/useTracker';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import RepubliqueHeader from '@/components/RepubliqueHeader';
|
||||
|
||||
export default function DashboardScreen() {
|
||||
const router = useRouter();
|
||||
@@ -15,7 +16,9 @@ export default function DashboardScreen() {
|
||||
const recentSaisies = (saisies || []).slice(0, 3);
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.screen}>
|
||||
<RepubliqueHeader />
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.greeting}>Bonjour, {user?.prenom} 👋</Text>
|
||||
<Text style={styles.subtitle}>Comment allez-vous aujourd'hui ?</Text>
|
||||
|
||||
@@ -80,40 +83,42 @@ export default function DashboardScreen() {
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: { flex: 1, backgroundColor: Colors.white },
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20 },
|
||||
greeting: { fontSize: 28, fontWeight: '800', color: Colors.black, marginTop: 8 },
|
||||
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 4, marginBottom: 24 },
|
||||
actionsRow: { flexDirection: 'row', gap: 12, marginBottom: 24 },
|
||||
actionCard: {
|
||||
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 20,
|
||||
alignItems: 'center', gap: 12, borderWidth: 1, borderColor: Colors.gray[100],
|
||||
flex: 1, backgroundColor: Colors.white, borderRadius: 4, padding: 20,
|
||||
alignItems: 'center', gap: 12, borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
actionIcon: { width: 56, height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
|
||||
actionText: { fontSize: 14, fontWeight: '600', color: Colors.gray[700] },
|
||||
actionIcon: { width: 56, height: 56, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
|
||||
actionText: { fontSize: 14, fontWeight: '600', color: Colors.black },
|
||||
statsContainer: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '700', color: Colors.black, marginBottom: 12 },
|
||||
statsRow: { flexDirection: 'row', gap: 12 },
|
||||
statCard: {
|
||||
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 16,
|
||||
alignItems: 'center', borderWidth: 1, borderColor: Colors.gray[100],
|
||||
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 4, padding: 16,
|
||||
alignItems: 'center', borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
statValue: { fontSize: 24, fontWeight: '800', color: Colors.black },
|
||||
statValue: { fontSize: 24, fontWeight: '800', color: Colors.primary },
|
||||
statLabel: { fontSize: 12, color: Colors.gray[500], marginTop: 4 },
|
||||
recentContainer: { marginBottom: 24 },
|
||||
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
|
||||
seeAll: { fontSize: 14, fontWeight: '600', color: Colors.secondary },
|
||||
seeAll: { fontSize: 14, fontWeight: '600', color: Colors.primary, textDecorationLine: 'underline' },
|
||||
saisieItem: {
|
||||
flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14,
|
||||
backgroundColor: Colors.gray[50], borderRadius: 14, marginBottom: 8,
|
||||
borderWidth: 1, borderColor: Colors.gray[100],
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 8,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
emotionIcon: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionIcon: { width: 44, height: 44, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionEmoji: { fontSize: 22 },
|
||||
saisieContent: { flex: 1 },
|
||||
saisieEmotion: { fontSize: 15, fontWeight: '600', color: Colors.black },
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function FeedDetailScreen() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.secondary} />
|
||||
<ActivityIndicator size="large" color={Colors.primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export default function FeedDetailScreen() {
|
||||
<Stack.Screen options={{ title: feed.titre ?? '' }} />
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.badge}>
|
||||
<Ionicons name="information-circle" size={14} color={Colors.secondary} />
|
||||
<Ionicons name="information-circle" size={14} color={Colors.primary} />
|
||||
<Text style={styles.badgeText}>Information santé mentale</Text>
|
||||
</View>
|
||||
|
||||
@@ -52,10 +52,10 @@ const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.white },
|
||||
badge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', backgroundColor: `${Colors.secondary}15`, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 999, marginBottom: 12 },
|
||||
badgeText: { fontSize: 12, fontWeight: '600', color: Colors.secondary },
|
||||
badge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', backgroundColor: `${Colors.primary}15`, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, marginBottom: 12 },
|
||||
badgeText: { fontSize: 12, fontWeight: '600', color: Colors.primary },
|
||||
title: { fontSize: 24, fontWeight: '800', color: Colors.black, marginBottom: 8, lineHeight: 30 },
|
||||
date: { fontSize: 13, color: Colors.gray[400], marginBottom: 20 },
|
||||
body: { fontSize: 15, lineHeight: 24, color: Colors.gray[700] ?? '#374151' },
|
||||
body: { fontSize: 15, lineHeight: 24, color: Colors.black },
|
||||
errorText: { marginTop: 12, fontSize: 16, color: Colors.gray[400] },
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function InfoListScreen() {
|
||||
onPress={() => router.push(`/(tabs)/info/${item.slug}`)}
|
||||
>
|
||||
<View style={styles.cardIcon}>
|
||||
<Ionicons name="document-text" size={24} color={Colors.secondary} />
|
||||
<Ionicons name="document-text" size={24} color={Colors.primary} />
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardTitle}>{item.titre}</Text>
|
||||
@@ -62,14 +62,14 @@ const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
listContent: { padding: 16 },
|
||||
header: { marginBottom: 20 },
|
||||
headerTitle: { fontSize: 14, fontWeight: '700', color: Colors.secondary, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
headerTitle: { fontSize: 14, fontWeight: '700', color: Colors.primary, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
headerSubtitle: { fontSize: 15, color: Colors.gray[500], marginTop: 4 },
|
||||
card: {
|
||||
flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16,
|
||||
backgroundColor: Colors.gray[50], borderRadius: 16, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[100],
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
cardIcon: { width: 48, height: 48, borderRadius: 14, backgroundColor: `${Colors.secondary}15`, justifyContent: 'center', alignItems: 'center' },
|
||||
cardIcon: { width: 48, height: 48, borderRadius: 4, backgroundColor: `${Colors.primary}15`, justifyContent: 'center', alignItems: 'center' },
|
||||
cardContent: { flex: 1 },
|
||||
cardTitle: { fontSize: 15, fontWeight: '700', color: Colors.black, marginBottom: 4 },
|
||||
cardExcerpt: { fontSize: 13, color: Colors.gray[500], lineHeight: 18 },
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function EditEntryScreen() {
|
||||
onPress={handleSubmit}
|
||||
disabled={updateSaisie.isPending || !emotionId}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color={Colors.black} />
|
||||
<Ionicons name="checkmark" size={20} color={Colors.white} />
|
||||
<Text style={styles.submitBtnText}>
|
||||
{updateSaisie.isPending ? 'Envoi...' : 'Enregistrer'}
|
||||
</Text>
|
||||
@@ -129,34 +129,35 @@ const styles = StyleSheet.create({
|
||||
loadingText: { color: Colors.gray[400], fontSize: 16 },
|
||||
title: { fontSize: 24, fontWeight: '800', color: Colors.black, marginBottom: 24 },
|
||||
section: { marginBottom: 24 },
|
||||
label: { fontSize: 15, fontWeight: '700', color: Colors.gray[700], marginBottom: 10 },
|
||||
label: { fontSize: 15, fontWeight: '700', color: Colors.black, marginBottom: 10 },
|
||||
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 16 },
|
||||
intensityValue: { fontSize: 48, fontWeight: '800', color: Colors.black },
|
||||
intensityUnit: { fontSize: 20, color: Colors.gray[400], marginLeft: 4 },
|
||||
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
|
||||
sliderDot: {
|
||||
width: 28, height: 28, borderRadius: 14,
|
||||
width: 28, height: 28, borderRadius: 4,
|
||||
backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
sliderDotActive: { backgroundColor: Colors.primary },
|
||||
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
|
||||
sliderDotTextActive: { color: Colors.black },
|
||||
sliderDotTextActive: { color: Colors.white },
|
||||
sliderLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 },
|
||||
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
|
||||
textArea: {
|
||||
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
|
||||
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black, minHeight: 100,
|
||||
backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4,
|
||||
borderBottomWidth: 2, borderBottomColor: Colors.black,
|
||||
padding: 16, fontSize: 15, color: Colors.black, minHeight: 100,
|
||||
},
|
||||
actionsRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, gap: 12 },
|
||||
cancelBtn: {
|
||||
flex: 1, paddingVertical: 16, borderRadius: 14,
|
||||
backgroundColor: Colors.gray[100], alignItems: 'center',
|
||||
flex: 1, paddingVertical: 16, borderRadius: 4,
|
||||
backgroundColor: Colors.white, alignItems: 'center', borderWidth: 1, borderColor: Colors.primary,
|
||||
},
|
||||
cancelBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
|
||||
cancelBtnText: { fontSize: 15, fontWeight: '600', color: Colors.primary },
|
||||
submitBtn: {
|
||||
flex: 1, flexDirection: 'row', gap: 8, paddingVertical: 16, borderRadius: 14,
|
||||
backgroundColor: Colors.secondary, alignItems: 'center', justifyContent: 'center',
|
||||
flex: 1, flexDirection: 'row', gap: 8, paddingVertical: 16, borderRadius: 4,
|
||||
backgroundColor: Colors.primary, alignItems: 'center', justifyContent: 'center',
|
||||
},
|
||||
submitBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
|
||||
submitBtnText: { fontSize: 15, fontWeight: '700', color: Colors.white },
|
||||
btnDisabled: { opacity: 0.5 },
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function JournalScreen() {
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity style={styles.fab} onPress={() => router.push('/(tabs)/journal/new')}>
|
||||
<Ionicons name="add" size={28} color={Colors.black} />
|
||||
<Ionicons name="add" size={28} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
@@ -92,14 +92,14 @@ const styles = StyleSheet.create({
|
||||
listContent: { padding: 16, paddingBottom: 80 },
|
||||
saisieCard: {
|
||||
flexDirection: 'row', alignItems: 'flex-start', gap: 12, padding: 16,
|
||||
backgroundColor: Colors.gray[50], borderRadius: 16, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[100],
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
emotionIcon: { width: 48, height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionIcon: { width: 48, height: 48, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionEmoji: { fontSize: 24 },
|
||||
saisieContent: { flex: 1 },
|
||||
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 6 },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 8 },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 4 },
|
||||
badgeText: { fontSize: 13, fontWeight: '600' },
|
||||
intensityText: { fontSize: 12, color: Colors.gray[400] },
|
||||
intensityBar: { height: 4, backgroundColor: Colors.gray[200], borderRadius: 2, marginBottom: 6 },
|
||||
@@ -113,7 +113,7 @@ const styles = StyleSheet.create({
|
||||
emptyText: { fontSize: 14, color: Colors.gray[400], marginTop: 4 },
|
||||
fab: {
|
||||
position: 'absolute', right: 20, bottom: 20,
|
||||
width: 56, height: 56, borderRadius: 28,
|
||||
width: 56, height: 56, borderRadius: 4,
|
||||
backgroundColor: Colors.primary, justifyContent: 'center', alignItems: 'center',
|
||||
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8,
|
||||
elevation: 6,
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function NewEntryScreen() {
|
||||
disabled={!emotionId}
|
||||
>
|
||||
<Text style={styles.nextBtnText}>Suivant</Text>
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
@@ -96,12 +96,12 @@ export default function NewEntryScreen() {
|
||||
</View>
|
||||
<View style={styles.navRow}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(1)}>
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.primary} />
|
||||
<Text style={styles.backBtnText}>Retour</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.nextBtn} onPress={() => setStep(3)}>
|
||||
<Text style={styles.nextBtnText}>Suivant</Text>
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
|
||||
<Ionicons name="arrow-forward" size={20} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -122,7 +122,7 @@ export default function NewEntryScreen() {
|
||||
/>
|
||||
<View style={styles.navRow}>
|
||||
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(2)}>
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
|
||||
<Ionicons name="arrow-back" size={20} color={Colors.primary} />
|
||||
<Text style={styles.backBtnText}>Retour</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
@@ -130,7 +130,7 @@ export default function NewEntryScreen() {
|
||||
onPress={handleSubmit}
|
||||
disabled={createSaisie.isPending}
|
||||
>
|
||||
<Ionicons name="checkmark" size={20} color={Colors.black} />
|
||||
<Ionicons name="checkmark" size={20} color={Colors.white} />
|
||||
<Text style={styles.nextBtnText}>{createSaisie.isPending ? 'Envoi...' : 'Enregistrer'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -149,11 +149,11 @@ const styles = StyleSheet.create({
|
||||
width: 32, height: 32, borderRadius: 16, backgroundColor: Colors.gray[200],
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
},
|
||||
stepActive: { backgroundColor: Colors.secondary },
|
||||
stepActive: { backgroundColor: Colors.primary },
|
||||
stepNumber: { fontSize: 14, fontWeight: '700', color: Colors.gray[500] },
|
||||
stepNumberActive: { color: Colors.white },
|
||||
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: 4 },
|
||||
stepLineActive: { backgroundColor: Colors.secondary },
|
||||
stepLineActive: { backgroundColor: Colors.primary },
|
||||
stepTitle: { fontSize: 20, fontWeight: '700', color: Colors.black, marginBottom: 20 },
|
||||
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 24 },
|
||||
intensityValue: { fontSize: 64, fontWeight: '800', color: Colors.black },
|
||||
@@ -165,20 +165,21 @@ const styles = StyleSheet.create({
|
||||
sliderTrack: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 8 },
|
||||
sliderFill: { height: 6, backgroundColor: Colors.primary, borderRadius: 3 },
|
||||
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
sliderDot: { width: 28, height: 28, borderRadius: 14, backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center' },
|
||||
sliderDot: { width: 28, height: 28, borderRadius: 4, backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center' },
|
||||
sliderDotActive: { backgroundColor: Colors.primary },
|
||||
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
|
||||
sliderDotTextActive: { color: Colors.black },
|
||||
sliderDotTextActive: { color: Colors.white },
|
||||
textArea: {
|
||||
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
|
||||
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black,
|
||||
backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4,
|
||||
borderBottomWidth: 2, borderBottomColor: Colors.black,
|
||||
padding: 16, fontSize: 15, color: Colors.black,
|
||||
minHeight: 120, marginBottom: 24,
|
||||
},
|
||||
navRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 16 },
|
||||
backBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 20, borderRadius: 12, backgroundColor: Colors.gray[100] },
|
||||
backBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
|
||||
nextBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.primary },
|
||||
submitBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.secondary },
|
||||
nextBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
|
||||
backBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 20, borderRadius: 4, backgroundColor: Colors.white, borderWidth: 1, borderColor: Colors.primary },
|
||||
backBtnText: { fontSize: 15, fontWeight: '600', color: Colors.primary },
|
||||
nextBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 4, backgroundColor: Colors.primary },
|
||||
submitBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 4, backgroundColor: Colors.primary },
|
||||
nextBtnText: { fontSize: 15, fontWeight: '700', color: Colors.white },
|
||||
btnDisabled: { opacity: 0.5 },
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function ReportsScreen() {
|
||||
<Text style={styles.statLabel}>Intensite moy.</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Ionicons name="heart" size={20} color="#FF69B4" />
|
||||
<Ionicons name="heart" size={20} color={Colors.rougeMarianne} />
|
||||
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
|
||||
<Text style={styles.statLabel}>{rapport.stats.emotion_dominante?.nom || 'Aucune'}</Text>
|
||||
</View>
|
||||
@@ -102,17 +102,17 @@ export default function ReportsScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 12, padding: 4, marginBottom: 20 },
|
||||
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 10 },
|
||||
periodBtnActive: { backgroundColor: Colors.white, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 },
|
||||
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 4, padding: 4, marginBottom: 20 },
|
||||
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 4 },
|
||||
periodBtnActive: { backgroundColor: Colors.primary },
|
||||
periodText: { fontSize: 13, fontWeight: '600', color: Colors.gray[500] },
|
||||
periodTextActive: { color: Colors.black },
|
||||
periodTextActive: { color: Colors.white },
|
||||
loading: { padding: 40, alignItems: 'center' },
|
||||
loadingText: { color: Colors.gray[400] },
|
||||
statsRow: { flexDirection: 'row', gap: 10, marginBottom: 16 },
|
||||
statCard: {
|
||||
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 16,
|
||||
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[100],
|
||||
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 4, padding: 16,
|
||||
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
statValue: { fontSize: 22, fontWeight: '800', color: Colors.black },
|
||||
statLabel: { fontSize: 11, color: Colors.gray[500] },
|
||||
@@ -123,7 +123,7 @@ const styles = StyleSheet.create({
|
||||
repartitionItem: { marginBottom: 16 },
|
||||
repartitionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 6 },
|
||||
colorDot: { width: 10, height: 10, borderRadius: 5, marginRight: 8 },
|
||||
repartitionName: { flex: 1, fontSize: 14, fontWeight: '600', color: Colors.gray[700] },
|
||||
repartitionName: { flex: 1, fontSize: 14, fontWeight: '600', color: Colors.black },
|
||||
repartitionPct: { fontSize: 14, fontWeight: '700', color: Colors.black },
|
||||
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 4 },
|
||||
progressFill: { height: 6, borderRadius: 3 },
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function ProfileScreen() {
|
||||
|
||||
{/* Logout */}
|
||||
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout}>
|
||||
<Ionicons name="log-out-outline" size={20} color="#DC143C" />
|
||||
<Ionicons name="log-out-outline" size={20} color={Colors.error} />
|
||||
<Text style={styles.logoutText}>Se deconnecter</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -82,26 +82,26 @@ const styles = StyleSheet.create({
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
profileHeader: { alignItems: 'center', marginBottom: 32, marginTop: 12 },
|
||||
avatar: {
|
||||
width: 80, height: 80, borderRadius: 40, backgroundColor: Colors.primary,
|
||||
width: 80, height: 80, borderRadius: 4, backgroundColor: Colors.primary,
|
||||
justifyContent: 'center', alignItems: 'center', marginBottom: 12,
|
||||
},
|
||||
avatarText: { fontSize: 28, fontWeight: '800', color: Colors.black },
|
||||
avatarText: { fontSize: 28, fontWeight: '800', color: Colors.white },
|
||||
name: { fontSize: 22, fontWeight: '700', color: Colors.black },
|
||||
email: { fontSize: 14, color: Colors.gray[500], marginTop: 4 },
|
||||
roleBadge: { backgroundColor: `${Colors.secondary}20`, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
|
||||
roleText: { fontSize: 12, fontWeight: '600', color: Colors.secondary, textTransform: 'capitalize' },
|
||||
roleBadge: { backgroundColor: `${Colors.primary}15`, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 4, marginTop: 8 },
|
||||
roleText: { fontSize: 12, fontWeight: '600', color: Colors.primary, textTransform: 'capitalize' },
|
||||
menuSection: { marginBottom: 24 },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '700', color: Colors.gray[400], textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 },
|
||||
menuCard: { backgroundColor: Colors.gray[50], borderRadius: 16, overflow: 'hidden', borderWidth: 1, borderColor: Colors.gray[100] },
|
||||
sectionTitle: { fontSize: 13, fontWeight: '700', color: Colors.gray[500], textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 },
|
||||
menuCard: { backgroundColor: Colors.white, borderRadius: 4, overflow: 'hidden', borderWidth: 1, borderColor: Colors.gray[200] },
|
||||
menuItem: {
|
||||
flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 15, paddingHorizontal: 16,
|
||||
borderBottomWidth: 1, borderBottomColor: Colors.gray[100],
|
||||
borderBottomWidth: 1, borderBottomColor: Colors.gray[200],
|
||||
},
|
||||
menuLabel: { flex: 1, fontSize: 15, color: Colors.gray[700] },
|
||||
menuLabel: { flex: 1, fontSize: 15, color: Colors.black },
|
||||
logoutBtn: {
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||
paddingVertical: 16, borderRadius: 14, backgroundColor: '#DC143C10',
|
||||
paddingVertical: 16, borderRadius: 4, backgroundColor: Colors.errorBg,
|
||||
},
|
||||
logoutText: { fontSize: 15, fontWeight: '600', color: '#DC143C' },
|
||||
logoutText: { fontSize: 15, fontWeight: '600', color: Colors.error },
|
||||
version: { textAlign: 'center', fontSize: 12, color: Colors.gray[400], marginTop: 20 },
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function UrgenceScreen() {
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconBadge}>
|
||||
<Ionicons name="heart" size={20} color="#DC2626" />
|
||||
<Ionicons name="heart" size={20} color={Colors.error} />
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>Vous n'êtes pas seul(e)</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
@@ -79,7 +79,7 @@ export default function UrgenceScreen() {
|
||||
{CONTACTS.map((c) => (
|
||||
<TouchableOpacity key={c.numero} style={styles.card} onPress={() => appeler(c.numero, c.nom)}>
|
||||
<View style={styles.cardIcon}>
|
||||
<Ionicons name="call" size={22} color="#DC2626" />
|
||||
<Ionicons name="call" size={22} color={Colors.error} />
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardName}>{c.nom}</Text>
|
||||
@@ -92,7 +92,7 @@ export default function UrgenceScreen() {
|
||||
))}
|
||||
|
||||
<View style={styles.warning}>
|
||||
<Ionicons name="warning" size={18} color="#DC2626" />
|
||||
<Ionicons name="warning" size={18} color={Colors.error} />
|
||||
<Text style={styles.warningText}>
|
||||
En cas d'urgence vitale, appelez immédiatement le <Text style={styles.bold}>15</Text> (SAMU) ou le <Text style={styles.bold}>112</Text>.
|
||||
</Text>
|
||||
@@ -105,21 +105,21 @@ const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 16, paddingBottom: 32 },
|
||||
header: { marginBottom: 20, alignItems: 'flex-start' },
|
||||
iconBadge: { width: 40, height: 40, borderRadius: 999, backgroundColor: '#FEE2E2', justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||
iconBadge: { width: 40, height: 40, borderRadius: 4, backgroundColor: Colors.errorBg, justifyContent: 'center', alignItems: 'center', marginBottom: 12 },
|
||||
headerTitle: { fontSize: 22, fontWeight: '800', color: Colors.black, marginBottom: 6 },
|
||||
headerSubtitle: { fontSize: 14, color: Colors.gray[500], lineHeight: 20 },
|
||||
card: {
|
||||
flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16,
|
||||
backgroundColor: Colors.gray[50], borderRadius: 16, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[100],
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: '#FEE2E2', justifyContent: 'center', alignItems: 'center' },
|
||||
cardIcon: { width: 44, height: 44, borderRadius: 4, backgroundColor: Colors.errorBg, justifyContent: 'center', alignItems: 'center' },
|
||||
cardContent: { flex: 1 },
|
||||
cardName: { fontSize: 14, fontWeight: '700', color: Colors.black, marginBottom: 2 },
|
||||
cardNumber: { fontSize: 20, fontWeight: '800', color: '#DC2626', marginBottom: 6 },
|
||||
cardNumber: { fontSize: 20, fontWeight: '800', color: Colors.error, marginBottom: 6 },
|
||||
cardDescription: { fontSize: 12, color: Colors.gray[500], lineHeight: 17 },
|
||||
cardAvailability: { fontSize: 11, color: Colors.secondary, marginTop: 4, fontWeight: '600' },
|
||||
warning: { marginTop: 8, padding: 14, borderRadius: 12, backgroundColor: '#FEF2F2', borderWidth: 1, borderColor: '#FECACA', flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
|
||||
warningText: { flex: 1, fontSize: 13, color: '#B91C1C', lineHeight: 19 },
|
||||
cardAvailability: { fontSize: 11, color: Colors.primary, marginTop: 4, fontWeight: '600' },
|
||||
warning: { marginTop: 8, padding: 14, borderRadius: 4, backgroundColor: Colors.errorBg, borderWidth: 1, borderColor: Colors.error, flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
|
||||
warningText: { flex: 1, fontSize: 13, color: Colors.error, lineHeight: 19 },
|
||||
bold: { fontWeight: '800' },
|
||||
});
|
||||
|
||||
@@ -33,7 +33,7 @@ function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}>
|
||||
<ActivityIndicator size="large" color="#fce117" />
|
||||
<ActivityIndicator size="large" color="#000091" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ForgotPasswordScreen() {
|
||||
<View style={styles.container}>
|
||||
<View style={styles.success}>
|
||||
<View style={styles.successIcon}>
|
||||
<Ionicons name="mail-open" size={32} color={Colors.secondary} />
|
||||
<Ionicons name="mail-open" size={32} color={Colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.successTitle}>Demande enregistrée</Text>
|
||||
<Text style={styles.successText}>
|
||||
@@ -95,17 +95,17 @@ const styles = StyleSheet.create({
|
||||
header: { alignItems: 'center', marginBottom: 32 },
|
||||
logo: { fontSize: 26, fontWeight: '800', color: Colors.black, textAlign: 'center' },
|
||||
subtitle: { fontSize: 14, color: Colors.gray[500], marginTop: 10, textAlign: 'center', lineHeight: 20 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 24, padding: 24 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.gray[700], marginBottom: 6 },
|
||||
input: { backgroundColor: Colors.white, borderWidth: 1, borderColor: Colors.gray[300], borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
|
||||
errorText: { color: '#DC2626', marginTop: 6, fontSize: 12 },
|
||||
button: { backgroundColor: Colors.primary, borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 8 },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
|
||||
input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
|
||||
errorText: { color: Colors.error, marginTop: 6, fontSize: 12 },
|
||||
button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 8 },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
|
||||
footer: { alignItems: 'center', marginTop: 20 },
|
||||
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
|
||||
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
|
||||
success: { padding: 24, alignItems: 'center', flex: 1, justifyContent: 'center' },
|
||||
successIcon: { width: 72, height: 72, borderRadius: 999, backgroundColor: `${Colors.secondary}15`, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
successIcon: { width: 72, height: 72, borderRadius: 999, backgroundColor: `${Colors.primary}15`, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
|
||||
successTitle: { fontSize: 22, fontWeight: '800', color: Colors.black, marginBottom: 12 },
|
||||
successText: { fontSize: 14, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: 12 },
|
||||
bold: { fontWeight: '700', color: Colors.black },
|
||||
|
||||
+13
-10
@@ -4,6 +4,7 @@ import { Link, useRouter } from 'expo-router';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { getApiError } from '@/lib/api';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import RepubliqueHeader from '@/components/RepubliqueHeader';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const router = useRouter();
|
||||
@@ -30,6 +31,7 @@ export default function LoginScreen() {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
||||
<RepubliqueHeader />
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.logo}>CESIZen</Text>
|
||||
@@ -95,17 +97,18 @@ const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
|
||||
header: { alignItems: 'center', marginBottom: 40 },
|
||||
logo: { fontSize: 36, fontWeight: '800', color: Colors.black },
|
||||
logo: { fontSize: 36, fontWeight: '800', color: Colors.primary },
|
||||
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 24, padding: 24 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
|
||||
title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.gray[700], marginBottom: 6 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
|
||||
input: {
|
||||
backgroundColor: Colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: Colors.gray[300],
|
||||
borderRadius: 12,
|
||||
backgroundColor: Colors.gray[100],
|
||||
borderTopLeftRadius: 4,
|
||||
borderTopRightRadius: 4,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: Colors.black,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
fontSize: 16,
|
||||
@@ -113,15 +116,15 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
button: {
|
||||
backgroundColor: Colors.primary,
|
||||
borderRadius: 12,
|
||||
borderRadius: 4,
|
||||
paddingVertical: 16,
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
buttonDisabled: { opacity: 0.6 },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
|
||||
forgotLink: { alignItems: 'center', marginTop: 12 },
|
||||
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 16 },
|
||||
footerText: { color: Colors.gray[500], fontSize: 14 },
|
||||
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
|
||||
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthStore } from '@/lib/auth-store';
|
||||
import { getApiError } from '@/lib/api';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import RepubliqueHeader from '@/components/RepubliqueHeader';
|
||||
|
||||
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
@@ -61,6 +62,7 @@ export default function RegisterScreen() {
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
|
||||
<RepubliqueHeader />
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.logo}>CESIZen</Text>
|
||||
@@ -132,24 +134,24 @@ const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
|
||||
header: { alignItems: 'center', marginBottom: 32 },
|
||||
logo: { fontSize: 36, fontWeight: '800', color: Colors.black },
|
||||
logo: { fontSize: 36, fontWeight: '800', color: Colors.primary },
|
||||
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 24, padding: 24 },
|
||||
form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
|
||||
title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
|
||||
row: { flexDirection: 'row' },
|
||||
inputGroup: { marginBottom: 16 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.gray[700], marginBottom: 6 },
|
||||
input: { backgroundColor: Colors.white, borderWidth: 1, borderColor: Colors.gray[300], borderRadius: 12, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
|
||||
errorText: { color: '#DC2626', marginTop: 4, fontSize: 12 },
|
||||
label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
|
||||
input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
|
||||
errorText: { color: Colors.error, marginTop: 4, fontSize: 12 },
|
||||
consentRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, marginVertical: 12 },
|
||||
checkbox: { width: 22, height: 22, borderRadius: 6, borderWidth: 2, borderColor: Colors.gray[300], backgroundColor: Colors.white, justifyContent: 'center', alignItems: 'center', marginTop: 1 },
|
||||
checkboxChecked: { backgroundColor: Colors.secondary, borderColor: Colors.secondary },
|
||||
consentText: { flex: 1, fontSize: 13, color: Colors.gray[700], lineHeight: 18 },
|
||||
consentLink: { color: Colors.secondary, fontWeight: '600' },
|
||||
button: { backgroundColor: Colors.primary, borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 12 },
|
||||
checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, borderColor: Colors.black, backgroundColor: Colors.white, justifyContent: 'center', alignItems: 'center', marginTop: 1 },
|
||||
checkboxChecked: { backgroundColor: Colors.primary, borderColor: Colors.primary },
|
||||
consentText: { flex: 1, fontSize: 13, color: Colors.black, lineHeight: 18 },
|
||||
consentLink: { color: Colors.primary, fontWeight: '600', textDecorationLine: 'underline' },
|
||||
button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 12 },
|
||||
buttonDisabled: { opacity: 0.6 },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
|
||||
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
|
||||
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
|
||||
footerText: { color: Colors.gray[500], fontSize: 14 },
|
||||
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
|
||||
link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
// babel-preset-expo (SDK 54) ajoute automatiquement le plugin worklets
|
||||
// de Reanimated 4 ; pas besoin de l'ajouter manuellement.
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -87,13 +87,13 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
|
||||
const styles = StyleSheet.create({
|
||||
loading: { padding: 40, alignItems: 'center' },
|
||||
backBtn: { marginBottom: 12 },
|
||||
backText: { fontSize: 14, color: Colors.gray[500] },
|
||||
backText: { fontSize: 14, color: Colors.primary, textDecorationLine: 'underline' },
|
||||
subTitle: { fontSize: 14, color: Colors.gray[600], marginBottom: 12 },
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
|
||||
emotionCard: {
|
||||
width: '30%',
|
||||
backgroundColor: Colors.gray[50],
|
||||
borderRadius: 16,
|
||||
backgroundColor: Colors.white,
|
||||
borderRadius: 4,
|
||||
padding: 14,
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
@@ -101,6 +101,6 @@ const styles = StyleSheet.create({
|
||||
borderColor: Colors.gray[200],
|
||||
},
|
||||
emotionEmoji: { fontSize: 30 },
|
||||
emotionName: { fontSize: 12, fontWeight: '600', color: Colors.gray[700], textAlign: 'center' },
|
||||
emotionName: { fontSize: 12, fontWeight: '600', color: Colors.black, textAlign: 'center' },
|
||||
arrow: { fontSize: 10, color: Colors.gray[400] },
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
interface RepubliqueHeaderProps {
|
||||
/** Affiche le nom du service (CESIZen) à côté du bloc Marianne. */
|
||||
showService?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* En-tête institutionnel reprenant l'identité visuelle de l'État (DSFR) :
|
||||
* bloc « RÉPUBLIQUE / FRANÇAISE » accompagné du nom du service.
|
||||
*/
|
||||
export default function RepubliqueHeader({ showService = true }: RepubliqueHeaderProps) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.marianneBlock}>
|
||||
<Text style={styles.republique}>RÉPUBLIQUE</Text>
|
||||
<Text style={styles.francaise}>FRANÇAISE</Text>
|
||||
<Text style={styles.devise}>Liberté{'\n'}Égalité{'\n'}Fraternité</Text>
|
||||
</View>
|
||||
{showService && (
|
||||
<View style={styles.serviceBlock}>
|
||||
<Text style={styles.serviceName}>CESIZen</Text>
|
||||
<Text style={styles.serviceTagline}>Santé mentale</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: Colors.white,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: Colors.gray[200],
|
||||
},
|
||||
marianneBlock: {
|
||||
paddingRight: 16,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: Colors.gray[200],
|
||||
},
|
||||
republique: { fontSize: 12, fontWeight: '700', color: Colors.black, letterSpacing: 0.5, lineHeight: 14 },
|
||||
francaise: { fontSize: 12, fontWeight: '700', color: Colors.black, letterSpacing: 0.5, lineHeight: 14 },
|
||||
devise: { fontSize: 8, color: Colors.gray[500], marginTop: 4, lineHeight: 10, fontStyle: 'italic' },
|
||||
serviceBlock: { paddingLeft: 16 },
|
||||
serviceName: { fontSize: 18, fontWeight: '800', color: Colors.primary },
|
||||
serviceTagline: { fontSize: 11, color: Colors.gray[500], marginTop: 1 },
|
||||
});
|
||||
@@ -59,11 +59,11 @@ export const useAuthStore = create<AuthState>()((set) => ({
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
try {
|
||||
await api.post('/auth/logout');
|
||||
} catch {}
|
||||
// Déconnexion locale d'abord (instantanée et fiable, même hors-ligne).
|
||||
await SecureStore.deleteItemAsync('cesizen_token');
|
||||
set({ user: null, token: null, isAuthenticated: false });
|
||||
// Notifie le serveur en arrière-plan (best-effort, sans bloquer l'UI).
|
||||
api.post('/auth/logout').catch(() => {});
|
||||
},
|
||||
|
||||
fetchUser: async () => {
|
||||
|
||||
+31
-15
@@ -1,22 +1,38 @@
|
||||
// Palette inspirée du Système de Design de l'État (DSFR / gouv.fr).
|
||||
// Le DSFR est un framework web (HTML/CSS) ; ici on en applique uniquement
|
||||
// l'identité visuelle via les couleurs des StyleSheet React Native.
|
||||
export const Colors = {
|
||||
primary: '#fce117',
|
||||
primaryDark: '#e5cc15',
|
||||
secondary: '#06c656',
|
||||
secondaryDark: '#05a348',
|
||||
black: '#000000',
|
||||
// Bleu France — couleur primaire / actions
|
||||
primary: '#000091',
|
||||
primaryDark: '#1212FF', // bleu survol / accent
|
||||
blueFranceSun: '#6A6AF4',
|
||||
// Rouge Marianne — accent institutionnel / alertes
|
||||
secondary: '#000091', // conservé en bleu pour les accents secondaires institutionnels
|
||||
secondaryDark: '#1212FF',
|
||||
rougeMarianne: '#E1000F',
|
||||
black: '#161616', // texte principal DSFR
|
||||
white: '#FFFFFF',
|
||||
// Échelle de gris DSFR (du plus clair au plus foncé)
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f3f4f6',
|
||||
200: '#e5e7eb',
|
||||
300: '#d1d5db',
|
||||
400: '#9ca3af',
|
||||
500: '#6b7280',
|
||||
600: '#4b5563',
|
||||
700: '#374151',
|
||||
800: '#1f2937',
|
||||
900: '#111827',
|
||||
50: '#F6F6F6', // fond alternatif
|
||||
100: '#EEEEEE', // gris contrasté
|
||||
200: '#DDDDDD', // séparateurs / bordures
|
||||
300: '#CECECE',
|
||||
400: '#929292', // texte désactivé
|
||||
500: '#666666', // texte secondaire / mention
|
||||
600: '#3A3A3A',
|
||||
700: '#2A2A2A',
|
||||
800: '#1F1F1F',
|
||||
900: '#161616',
|
||||
},
|
||||
// États DSFR
|
||||
success: '#18753C',
|
||||
successBg: '#B8FEC9',
|
||||
error: '#CE0500',
|
||||
errorBg: '#FFE9E9',
|
||||
info: '#0063CB',
|
||||
warning: '#B34000',
|
||||
// Couleurs par émotion — issues du référentiel, conservées telles quelles
|
||||
emotions: {
|
||||
joie: '#FFD700',
|
||||
colere: '#DC143C',
|
||||
|
||||
Generated
+2441
-1931
File diff suppressed because it is too large
Load Diff
+20
-18
@@ -16,32 +16,34 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.0",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-navigation/bottom-tabs": "^7.3.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"axios": "^1.7.0",
|
||||
"expo": "~53.0.0",
|
||||
"expo-constants": "~17.1.8",
|
||||
"expo-font": "~13.3.0",
|
||||
"expo-linking": "~7.1.7",
|
||||
"expo-router": "~5.1.11",
|
||||
"expo-secure-store": "~14.2.0",
|
||||
"expo-status-bar": "~2.2.0",
|
||||
"react": "19.0.0",
|
||||
"react-native": "0.79.6",
|
||||
"react-native-gesture-handler": "~2.24.0",
|
||||
"react-native-reanimated": "~3.17.4",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "~4.11.1",
|
||||
"expo": "^54.0.35",
|
||||
"expo-constants": "~18.0.13",
|
||||
"expo-font": "~14.0.12",
|
||||
"expo-linking": "~8.0.12",
|
||||
"expo-router": "~6.0.24",
|
||||
"expo-secure-store": "~15.0.8",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-worklets": "0.5.1",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "~19.0.0",
|
||||
"@types/jest": "29.5.14",
|
||||
"@types/react": "~19.1.8",
|
||||
"babel-preset-expo": "~54.0.11",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.0",
|
||||
"typescript": "~5.8.0"
|
||||
"jest-expo": "~54.0.17",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -3,8 +3,13 @@
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user