From bfd35905c9e3dc2cadd6f39de37cd588c705280a Mon Sep 17 00:00:00 2001 From: Azmog <80716388+AzmogEx@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:09:20 +0200 Subject: [PATCH] feat: add mobile application structure with emotion picker and API integration - Added favicon and icon assets for the mobile app. - Implemented EmotionPicker component for selecting emotions with child options. - Created hooks for fetching emotions, feeds, and rapport data. - Established API service for handling requests and authentication. - Integrated Zustand for state management of user authentication. - Defined color palette for the application. - Set up TypeScript types for various entities including User, Emotion, and Feed. - Configured Babel and TypeScript for the mobile project. - Added package.json with necessary dependencies for Expo and React Native. --- CLAUDE.md | 110 +++++++++++ backend/.env.example | 2 +- backend/Dockerfile | 1 + .../Admin/AdminUtilisateurController.php | 6 +- .../app/Http/Controllers/ProfilController.php | 9 +- .../app/Http/Middleware/RoleMiddleware.php | 2 +- .../app/Http/Requests/StoreFeedRequest.php | 2 +- .../app/Http/Requests/StoreSaisieRequest.php | 2 +- .../Http/Requests/UpdatePasswordRequest.php | 31 +++ ...01_000005_create_saisie_trackers_table.php | 2 +- .../2024_01_01_000006_create_feeds_table.php | 2 +- docker-compose.yml | 30 +++ .../src/app/(admin)/admin/contenus/page.tsx | 7 +- .../src/app/(admin)/admin/dashboard/page.tsx | 2 +- .../src/app/(admin)/admin/emotions/page.tsx | 7 +- .../app/(admin)/admin/utilisateurs/page.tsx | 5 +- frontend/src/app/(auth)/connexion/page.tsx | 6 +- frontend/src/app/(auth)/inscription/page.tsx | 10 +- .../src/app/(protected)/dashboard/page.tsx | 2 +- frontend/src/app/(protected)/journal/page.tsx | 13 +- frontend/src/app/(protected)/profil/page.tsx | 15 +- .../app/(public)/contacts-urgence/page.tsx | 83 ++++++++ .../app/(public)/informations/[slug]/page.tsx | 6 +- .../src/app/(public)/informations/page.tsx | 6 +- .../app/(public)/mentions-legales/page.tsx | 73 +++++++ .../politique-de-confidentialite/page.tsx | 105 ++++++++++ .../components/features/EditSaisieModal.tsx | 122 ++++++++++++ frontend/src/lib/auth-store.ts | 2 +- mobile/.gitignore | 11 ++ mobile/app.json | 28 +++ mobile/app/(tabs)/_layout.tsx | 53 +++++ mobile/app/(tabs)/index.tsx | 121 ++++++++++++ mobile/app/(tabs)/info.tsx | 75 +++++++ mobile/app/(tabs)/journal/_layout.tsx | 18 ++ mobile/app/(tabs)/journal/edit.tsx | 162 +++++++++++++++ mobile/app/(tabs)/journal/index.tsx | 121 ++++++++++++ mobile/app/(tabs)/journal/new.tsx | 184 ++++++++++++++++++ mobile/app/(tabs)/journal/reports.tsx | 134 +++++++++++++ mobile/app/(tabs)/profile.tsx | 107 ++++++++++ mobile/app/_layout.tsx | 56 ++++++ mobile/app/auth/_layout.tsx | 5 + mobile/app/auth/login.tsx | 119 +++++++++++ mobile/app/auth/register.tsx | 111 +++++++++++ mobile/app/index.tsx | 7 + mobile/assets/adaptive-icon.png | Bin 0 -> 5337 bytes mobile/assets/favicon.png | Bin 0 -> 124 bytes mobile/assets/icon.png | Bin 0 -> 5337 bytes mobile/assets/splash-icon.png | Bin 0 -> 1881 bytes mobile/babel.config.js | 6 + mobile/components/EmotionPicker.tsx | 106 ++++++++++ mobile/hooks/useEmotions.ts | 13 ++ mobile/hooks/useFeeds.ts | 24 +++ mobile/hooks/useRapport.ts | 14 ++ mobile/hooks/useTracker.ts | 69 +++++++ mobile/lib/api.ts | 37 ++++ mobile/lib/auth-store.ts | 72 +++++++ mobile/lib/colors.ts | 29 +++ mobile/package.json | 37 ++++ mobile/tsconfig.json | 10 + mobile/types/index.ts | 87 +++++++++ 60 files changed, 2437 insertions(+), 42 deletions(-) create mode 100644 CLAUDE.md create mode 100644 backend/app/Http/Requests/UpdatePasswordRequest.php create mode 100644 frontend/src/app/(public)/contacts-urgence/page.tsx create mode 100644 frontend/src/app/(public)/mentions-legales/page.tsx create mode 100644 frontend/src/app/(public)/politique-de-confidentialite/page.tsx create mode 100644 frontend/src/components/features/EditSaisieModal.tsx create mode 100644 mobile/.gitignore create mode 100644 mobile/app.json create mode 100644 mobile/app/(tabs)/_layout.tsx create mode 100644 mobile/app/(tabs)/index.tsx create mode 100644 mobile/app/(tabs)/info.tsx create mode 100644 mobile/app/(tabs)/journal/_layout.tsx create mode 100644 mobile/app/(tabs)/journal/edit.tsx create mode 100644 mobile/app/(tabs)/journal/index.tsx create mode 100644 mobile/app/(tabs)/journal/new.tsx create mode 100644 mobile/app/(tabs)/journal/reports.tsx create mode 100644 mobile/app/(tabs)/profile.tsx create mode 100644 mobile/app/_layout.tsx create mode 100644 mobile/app/auth/_layout.tsx create mode 100644 mobile/app/auth/login.tsx create mode 100644 mobile/app/auth/register.tsx create mode 100644 mobile/app/index.tsx create mode 100644 mobile/assets/adaptive-icon.png create mode 100644 mobile/assets/favicon.png create mode 100644 mobile/assets/icon.png create mode 100644 mobile/assets/splash-icon.png create mode 100644 mobile/babel.config.js create mode 100644 mobile/components/EmotionPicker.tsx create mode 100644 mobile/hooks/useEmotions.ts create mode 100644 mobile/hooks/useFeeds.ts create mode 100644 mobile/hooks/useRapport.ts create mode 100644 mobile/hooks/useTracker.ts create mode 100644 mobile/lib/api.ts create mode 100644 mobile/lib/auth-store.ts create mode 100644 mobile/lib/colors.ts create mode 100644 mobile/package.json create mode 100644 mobile/tsconfig.json create mode 100644 mobile/types/index.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..03c1c0a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,110 @@ +# CESIZen - Application de santé mentale + +## Contexte du projet +Projet individuel CESI École d'Ingénieurs — Bloc "Concevoir les solutions logicielles" (INFCDAAL1). +Commanditaire fictif : Ministère des Solidarités et de la Santé. +L'objectif est de proposer une plateforme grand public pour accompagner le quotidien de chacun et l'aider à mieux comprendre les enjeux de sa santé mentale et agir sur son stress. + +## Module choisi +**Tracker d'émotions** (module facultatif au choix) + modules obligatoires (Comptes utilisateurs, Informations). + +## Architecture technique + +### Stack +- **Backend** : Laravel 11 (PHP 8.2+) — API REST JSON, JWT Auth (`tymon/jwt-auth`) +- **Frontend Web** : Next.js 16 (React 19, TypeScript, Tailwind CSS 4, Zustand, TanStack Query, Recharts) +- **Mobile natif** : React Native / Expo 53 (Expo Router, Zustand, SecureStore, TanStack Query) +- **Base de données** : PostgreSQL 16 +- **Conteneurisation** : Docker Compose (postgres:5432, backend:8000, frontend:3000) + +### Design Pattern +MVC côté backend (Models → Controllers → Routes, Service Layer pour les rapports). +Frontend : App Router, Custom Hooks, Component Composition, Zustand Store. + +### Structure du projet +``` +Cesizen/ +├── backend/ # Laravel 11 API +├── frontend/ # Next.js (web) +├── mobile/ # React Native Expo (iOS + Android) +├── docker-compose.yml +└── generate_pdf.py # Génération du cahier de tests (Python/fpdf) +``` + +## Fonctionnalités implémentées + +### Modules obligatoires +- **Comptes utilisateurs** : inscription, connexion JWT, gestion profil, reset password, admin CRUD, désactivation/suppression, RGPD (soft delete + droit à la suppression) +- **Informations** : pages de contenu santé mentale (feeds), CRUD admin, affichage public + +### Module au choix : Tracker d'émotions +| Fonctionnalité | Acteur | Web | Mobile | +|---|---|---|---| +| Afficher le journal de bord | Utilisateur | ✅ | ✅ | +| Ajouter une saisie (wizard 3 étapes) | Utilisateur | ✅ | ✅ | +| Modifier une saisie | Utilisateur | ✅ (modale) | ✅ (écran edit) | +| Supprimer une saisie | Utilisateur | ✅ | ✅ | +| Rapports par période (semaine/mois/trimestre/année) | Utilisateur | ✅ (graphiques) | ✅ | +| Configurer les émotions (2 niveaux) | Admin | ✅ | N/A | + +### Hiérarchie des émotions (seed) +7 émotions de base (niveau 1) avec 31 sous-émotions (niveau 2) : +Joie, Tristesse, Colère, Peur, Dégoût, Surprise, Amour. + +## Grille de priorisation des besoins +Critères utilisés : Complexité (0-3), Valeur ajoutée Ministère (0-3), Valeur ajoutée Utilisateur (0-3), Nécessité (0-2), Interdépendance (0-2). Total sur 15. + +Top priorités : +- Diagnostic de stress / Journal émotions / Rapports d'émotions (10/15) +- Cryptage données / Inscription / Cohérence cardiaque / Activités détente / Ajout tracker (9/15) +- Conformité RGPD (8/15) + +## Évaluation — Barème (/34 → note lettre) +| Domaine | Livrable | Pts | +|---|---|---| +| Recueil du besoin | Reformulation (contexte, enjeux, contraintes) | 6 | +| | Identification des besoins logiciels | 3 | +| | Critères de priorités et pondération | 2 | +| Analyse des besoins | Complétude du CdC, contraintes, opportunités | 4 | +| | Réponse fonctionnelle adaptée et originale | 4 | +| | Schématisation et reformulation visuelle | 2 | +| Modélisation | MCD (Merise/UML) | 3 | +| | Conception technique MVC | 2 | +| | Spécifications fonctionnelles et techniques | 4 | +| | Données personnelles et sensibles | 1 | +| Dossier et soutenance | Qualité dossier + oral (20 min + 10 Q/R) | 3 | + +Conversion : A (27-34) | B (20-26,99) | C (14-19,99) | D (0-13,99) + +## Livrables attendus +1. **Cahier des charges** (15-20 pages) : contexte, parties prenantes, besoins reformulés, priorisation, contraintes +2. **Spécifications fonctionnelles et techniques** : UML, analyse détaillée (2 obligatoires + 1 au choix), MCD, MVC, RGPD +3. **Soutenance orale** : 20 min + 10 min Q/R + +## Acteurs du système +- **Visiteur anonyme** : consultation informations, accès public +- **Utilisateur connecté** : tracker émotions, journal, rapports, profil +- **Administrateur** : gestion utilisateurs, contenus, émotions + +## Conventions de code +- Backend : PHP PSR-12, noms en français (utilisateur, saisie_tracker, émotion) +- Frontend : TypeScript strict, composants fonctionnels, hooks customs +- Mobile : même conventions que le frontend web +- Langue de l'interface : français +- Langue de l'instruction CLAUDE.md globale : toujours répondre en français + +## Commandes utiles +```bash +# Backend +cd backend && php artisan serve # API sur :8000 +cd backend && php artisan migrate:fresh --seed # Reset + seed DB + +# Frontend Web +cd frontend && npm run dev # Dev server sur :3000 + +# Mobile +cd mobile && npm install && npx expo start # Expo dev server + +# Docker (tout lancer) +docker-compose up -d +``` diff --git a/backend/.env.example b/backend/.env.example index c0660ea..71447c3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,7 @@ APP_NAME=Laravel APP_ENV=local APP_KEY= -APP_DEBUG=true +APP_DEBUG=false APP_URL=http://localhost APP_LOCALE=en diff --git a/backend/Dockerfile b/backend/Dockerfile index 1ef54e8..e3d9bb7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y \ unzip \ git \ && docker-php-ext-install pdo pdo_pgsql bcmath \ + && pecl install redis && docker-php-ext-enable redis \ && rm -rf /var/lib/apt/lists/* # Composer diff --git a/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php b/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php index 02a301e..0df76db 100644 --- a/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php +++ b/backend/app/Http/Controllers/Admin/AdminUtilisateurController.php @@ -18,9 +18,9 @@ class AdminUtilisateurController extends Controller if ($request->filled('search')) { $search = $request->search; $query->where(function ($q) use ($search) { - $q->where('nom', 'ilike', "%{$search}%") - ->orWhere('prenom', 'ilike', "%{$search}%") - ->orWhere('email', 'ilike', "%{$search}%"); + $q->where('nom', 'like', "%{$search}%") + ->orWhere('prenom', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); }); } diff --git a/backend/app/Http/Controllers/ProfilController.php b/backend/app/Http/Controllers/ProfilController.php index f10d0a8..5f5b4a2 100644 --- a/backend/app/Http/Controllers/ProfilController.php +++ b/backend/app/Http/Controllers/ProfilController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdatePasswordRequest; use App\Http\Requests\UpdateProfilRequest; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; class ProfilController extends Controller @@ -29,13 +29,8 @@ class ProfilController extends Controller ]); } - public function updatePassword(Request $request): JsonResponse + public function updatePassword(UpdatePasswordRequest $request): JsonResponse { - $request->validate([ - 'ancien_mot_de_passe' => 'required|string', - 'password' => 'required|string|min:8|confirmed', - ]); - $utilisateur = auth()->user(); if (!Hash::check($request->ancien_mot_de_passe, $utilisateur->password)) { diff --git a/backend/app/Http/Middleware/RoleMiddleware.php b/backend/app/Http/Middleware/RoleMiddleware.php index d3a60ff..ce6f617 100644 --- a/backend/app/Http/Middleware/RoleMiddleware.php +++ b/backend/app/Http/Middleware/RoleMiddleware.php @@ -11,7 +11,7 @@ class RoleMiddleware { $user = auth()->user(); - if (!$user || !in_array($user->role->nom, $roles)) { + if (!$user || !$user->role || !in_array($user->role->nom, $roles)) { return response()->json(['message' => 'Accès non autorisé'], 403); } diff --git a/backend/app/Http/Requests/StoreFeedRequest.php b/backend/app/Http/Requests/StoreFeedRequest.php index c7599e0..ad2eb0e 100644 --- a/backend/app/Http/Requests/StoreFeedRequest.php +++ b/backend/app/Http/Requests/StoreFeedRequest.php @@ -17,7 +17,7 @@ class StoreFeedRequest extends FormRequest $rules = [ 'titre' => 'required|string|max:255', 'contenu' => 'required|string', - 'image_url' => 'nullable|string|max:500', + 'image_url' => 'nullable|url|max:500', 'est_publie' => 'boolean', 'ordre' => 'integer|min:0', ]; diff --git a/backend/app/Http/Requests/StoreSaisieRequest.php b/backend/app/Http/Requests/StoreSaisieRequest.php index 30380c0..51c534e 100644 --- a/backend/app/Http/Requests/StoreSaisieRequest.php +++ b/backend/app/Http/Requests/StoreSaisieRequest.php @@ -17,7 +17,7 @@ class StoreSaisieRequest extends FormRequest 'emotion_id' => 'required|exists:emotions,id', 'intensite' => 'required|integer|between:1,10', 'note' => 'nullable|string|max:2000', - 'date_saisie' => 'required|date', + 'date_saisie' => 'required|date_format:Y-m-d', ]; } diff --git a/backend/app/Http/Requests/UpdatePasswordRequest.php b/backend/app/Http/Requests/UpdatePasswordRequest.php new file mode 100644 index 0000000..5bcc37f --- /dev/null +++ b/backend/app/Http/Requests/UpdatePasswordRequest.php @@ -0,0 +1,31 @@ + 'required|string', + 'password' => 'required|string|min:8|confirmed', + ]; + } + + public function messages(): array + { + return [ + 'ancien_mot_de_passe.required' => 'L\'ancien mot de passe est obligatoire.', + 'password.required' => 'Le nouveau mot de passe est obligatoire.', + 'password.min' => 'Le nouveau mot de passe doit contenir au moins 8 caractères.', + 'password.confirmed' => 'La confirmation du mot de passe ne correspond pas.', + ]; + } +} diff --git a/backend/database/migrations/2024_01_01_000005_create_saisie_trackers_table.php b/backend/database/migrations/2024_01_01_000005_create_saisie_trackers_table.php index 9bb9e82..cb2d6a4 100644 --- a/backend/database/migrations/2024_01_01_000005_create_saisie_trackers_table.php +++ b/backend/database/migrations/2024_01_01_000005_create_saisie_trackers_table.php @@ -12,7 +12,7 @@ return new class extends Migration $table->id(); $table->foreignId('tracker_id')->constrained('trackers')->onDelete('cascade'); $table->foreignId('emotion_id')->constrained('emotions'); - $table->tinyInteger('intensite'); + $table->unsignedTinyInteger('intensite'); $table->text('note')->nullable(); $table->dateTime('date_saisie'); $table->timestamps(); diff --git a/backend/database/migrations/2024_01_01_000006_create_feeds_table.php b/backend/database/migrations/2024_01_01_000006_create_feeds_table.php index fce68f6..129161b 100644 --- a/backend/database/migrations/2024_01_01_000006_create_feeds_table.php +++ b/backend/database/migrations/2024_01_01_000006_create_feeds_table.php @@ -15,7 +15,7 @@ return new class extends Migration $table->longText('contenu'); $table->string('image_url')->nullable(); $table->boolean('est_publie')->default(false); - $table->foreignId('auteur_id')->constrained('utilisateurs'); + $table->foreignId('auteur_id')->constrained('utilisateurs')->onDelete('cascade'); $table->integer('ordre')->default(0); $table->timestamps(); }); diff --git a/docker-compose.yml b/docker-compose.yml index 5c0214c..7c88fe0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,27 @@ services: interval: 5s timeout: 5s retries: 5 + command: > + postgres + -c max_connections=200 + -c shared_buffers=256MB + -c effective_cache_size=768MB + -c work_mem=4MB + -c maintenance_work_mem=128MB + + redis: + image: redis:7-alpine + container_name: cesizen-redis + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 backend: build: ./backend @@ -24,6 +45,7 @@ services: ports: - "8000:8000" environment: + APP_NAME: CESIZen APP_ENV: local APP_DEBUG: "true" APP_KEY: base64:UMxXo/+o7v1n6E+kimssgw8L2lQ2NQGMuQffn+Vh7lc= @@ -36,11 +58,18 @@ services: DB_PASSWORD: cesizen_secret JWT_SECRET: w89wy05gyYCjGGBXyRHqthKEou5e40er2rdjg69j0mUCRL5ljescaC1CiMl8awtg FRONTEND_URL: http://localhost:3000 + CACHE_STORE: redis + SESSION_DRIVER: redis + QUEUE_CONNECTION: redis + REDIS_HOST: redis + REDIS_PORT: 6379 volumes: - ./backend:/app depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy frontend: build: ./frontend @@ -58,3 +87,4 @@ services: volumes: postgres_data: + redis_data: diff --git a/frontend/src/app/(admin)/admin/contenus/page.tsx b/frontend/src/app/(admin)/admin/contenus/page.tsx index 68bde95..340d0a8 100644 --- a/frontend/src/app/(admin)/admin/contenus/page.tsx +++ b/frontend/src/app/(admin)/admin/contenus/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Plus, Edit2, Trash2, Eye, EyeOff } from 'lucide-react'; +import { Plus, Edit2, Trash2 } from 'lucide-react'; import api from '@/lib/api'; import type { Feed } from '@/types'; import Card from '@/components/ui/Card'; @@ -69,8 +69,9 @@ export default function AdminContenusPage() { } setModalOpen(false); await fetchFeeds(); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur'); } finally { setFormLoading(false); } diff --git a/frontend/src/app/(admin)/admin/dashboard/page.tsx b/frontend/src/app/(admin)/admin/dashboard/page.tsx index b5dc859..6d4f564 100644 --- a/frontend/src/app/(admin)/admin/dashboard/page.tsx +++ b/frontend/src/app/(admin)/admin/dashboard/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import Link from 'next/link'; -import { Users, FileText, Heart, TrendingUp } from 'lucide-react'; +import { Users, FileText, Heart } from 'lucide-react'; import Card from '@/components/ui/Card'; import api from '@/lib/api'; diff --git a/frontend/src/app/(admin)/admin/emotions/page.tsx b/frontend/src/app/(admin)/admin/emotions/page.tsx index cd6187c..6ab69b0 100644 --- a/frontend/src/app/(admin)/admin/emotions/page.tsx +++ b/frontend/src/app/(admin)/admin/emotions/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Plus, Edit2, Trash2, Eye, EyeOff } from 'lucide-react'; +import { Plus, Edit2, Trash2 } from 'lucide-react'; import api from '@/lib/api'; import type { Emotion } from '@/types'; import Card from '@/components/ui/Card'; @@ -81,8 +81,9 @@ export default function AdminEmotionsPage() { } setModalOpen(false); await fetchEmotions(); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur'); } finally { setFormLoading(false); } diff --git a/frontend/src/app/(admin)/admin/utilisateurs/page.tsx b/frontend/src/app/(admin)/admin/utilisateurs/page.tsx index 5a24a2f..390cd8a 100644 --- a/frontend/src/app/(admin)/admin/utilisateurs/page.tsx +++ b/frontend/src/app/(admin)/admin/utilisateurs/page.tsx @@ -64,8 +64,9 @@ export default function AdminUtilisateursPage() { setNewUser({ nom: '', prenom: '', email: '', password: '', password_confirmation: '', role_id: 2 }); await fetchUsers(); toast.success('Utilisateur créé'); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur lors de la création'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur lors de la création'); } finally { setFormLoading(false); } diff --git a/frontend/src/app/(auth)/connexion/page.tsx b/frontend/src/app/(auth)/connexion/page.tsx index 4fea9d0..d5db484 100644 --- a/frontend/src/app/(auth)/connexion/page.tsx +++ b/frontend/src/app/(auth)/connexion/page.tsx @@ -23,8 +23,9 @@ export default function ConnexionPage() { await login(email, password); toast.success('Connexion réussie !'); router.push('/dashboard'); - } catch (error: any) { - const message = error?.response?.data?.message || 'Identifiants incorrects'; + } catch (error: unknown) { + const axiosError = error as import('axios').AxiosError<{ message?: string }>; + const message = axiosError.response?.data?.message || 'Identifiants incorrects'; toast.error(message); } finally { setLoading(false); @@ -83,6 +84,7 @@ export default function ConnexionPage() { 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 ? : } diff --git a/frontend/src/app/(auth)/inscription/page.tsx b/frontend/src/app/(auth)/inscription/page.tsx index 0043bc0..faef1a7 100644 --- a/frontend/src/app/(auth)/inscription/page.tsx +++ b/frontend/src/app/(auth)/inscription/page.tsx @@ -36,8 +36,9 @@ export default function InscriptionPage() { await register(form); toast.success('Inscription réussie ! Bienvenue sur CESIZen.'); router.push('/dashboard'); - } catch (error: any) { - const data = error?.response?.data; + } catch (error: unknown) { + const axiosError = error as import('axios').AxiosError<{ message?: string; errors?: Record }>; + const data = axiosError.response?.data; if (data?.errors) { setErrors(data.errors); } else { @@ -143,6 +144,7 @@ export default function InscriptionPage() { 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 ? : } @@ -181,8 +183,8 @@ export default function InscriptionPage() { /> diff --git a/frontend/src/app/(protected)/dashboard/page.tsx b/frontend/src/app/(protected)/dashboard/page.tsx index 43f0a0b..e496ab6 100644 --- a/frontend/src/app/(protected)/dashboard/page.tsx +++ b/frontend/src/app/(protected)/dashboard/page.tsx @@ -1,7 +1,7 @@ 'use client'; import Link from 'next/link'; -import { Heart, BarChart3, Plus, BookOpen, TrendingUp } from 'lucide-react'; +import { Heart, BarChart3, Plus, BookOpen } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { useSaisies } from '@/hooks/useTracker'; import { useRapport } from '@/hooks/useRapport'; diff --git a/frontend/src/app/(protected)/journal/page.tsx b/frontend/src/app/(protected)/journal/page.tsx index a113de1..595ed4e 100644 --- a/frontend/src/app/(protected)/journal/page.tsx +++ b/frontend/src/app/(protected)/journal/page.tsx @@ -2,14 +2,16 @@ import { useState } from 'react'; import Link from 'next/link'; -import { Plus, Filter, Calendar } from 'lucide-react'; +import { Plus, Filter } 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 toast from 'react-hot-toast'; +import type { SaisieTracker } from '@/types'; export default function JournalPage() { const [showFilters, setShowFilters] = useState(false); @@ -18,6 +20,7 @@ export default function JournalPage() { date_fin?: string; emotion_id?: number; }>({}); + const [editingSaisie, setEditingSaisie] = useState(null); const { data: saisies, isLoading } = useSaisies(filters); const { data: emotions } = useEmotions(); @@ -134,9 +137,17 @@ export default function JournalPage() { ) : ( setEditingSaisie(saisie)} onDelete={handleDelete} /> )} + + {/* Modale d'édition */} + setEditingSaisie(null)} + /> ); } diff --git a/frontend/src/app/(protected)/profil/page.tsx b/frontend/src/app/(protected)/profil/page.tsx index afc38bb..115f40e 100644 --- a/frontend/src/app/(protected)/profil/page.tsx +++ b/frontend/src/app/(protected)/profil/page.tsx @@ -37,8 +37,9 @@ export default function ProfilPage() { await fetchUser(); setEditing(false); toast.success('Profil mis à jour'); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur lors de la mise à jour'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur lors de la mise à jour'); } finally { setLoading(false); } @@ -52,8 +53,9 @@ export default function ProfilPage() { setChangingPassword(false); setPasswordForm({ ancien_mot_de_passe: '', password: '', password_confirmation: '' }); toast.success('Mot de passe modifié'); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur lors du changement'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur lors du changement'); } finally { setLoading(false); } @@ -65,8 +67,9 @@ export default function ProfilPage() { await api.delete('/profil'); toast.success('Compte supprimé'); logout(); - } catch (err: any) { - toast.error(err.response?.data?.message || 'Erreur lors de la suppression'); + } catch (err: unknown) { + const axiosError = err as import('axios').AxiosError<{ message?: string }>; + toast.error(axiosError.response?.data?.message || 'Erreur lors de la suppression'); } finally { setLoading(false); } diff --git a/frontend/src/app/(public)/contacts-urgence/page.tsx b/frontend/src/app/(public)/contacts-urgence/page.tsx new file mode 100644 index 0000000..0b8d86e --- /dev/null +++ b/frontend/src/app/(public)/contacts-urgence/page.tsx @@ -0,0 +1,83 @@ +import Card from '@/components/ui/Card'; +import { Phone } from 'lucide-react'; + +export default function ContactsUrgencePage() { + const contacts = [ + { + name: 'Numéro national de prévention du suicide', + number: '3114', + description: + 'Numéro national disponible 24h/24 et 7j/7. Appel gratuit et confidentiel pour les personnes en détresse psychologique et leur entourage.', + }, + { + name: 'SOS Amitié', + number: '09 72 39 40 50', + description: + 'Écoute et soutien pour les personnes en souffrance, solitude ou détresse morale. Service disponible 24h/24.', + }, + { + 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.', + }, + { + name: 'SAMU', + number: '15', + description: + 'Service d\u2019aide médicale urgente. En cas d\u2019urgence vitale, appelez immédiatement le 15 ou le 112.', + }, + { + name: 'SOS Médecins', + number: '3624', + description: + 'Service de médecins disponible pour des consultations à domicile, 24h/24 et 7j/7.', + }, + ]; + + return ( +
+

+ Contacts d'urgence +

+

+ 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). +

+ +
+ {contacts.map((contact) => ( + +
+
+ +
+
+

+ {contact.name} +

+ + {contact.number} + +

+ {contact.description} +

+
+
+
+ ))} +
+ + +

+ En cas d'urgence vitale, appelez immédiatement le{' '} + 15 (SAMU) ou le 112 (numéro d'urgence + européen). +

+
+
+ ); +} diff --git a/frontend/src/app/(public)/informations/[slug]/page.tsx b/frontend/src/app/(public)/informations/[slug]/page.tsx index 7f68fe4..e3e1de7 100644 --- a/frontend/src/app/(public)/informations/[slug]/page.tsx +++ b/frontend/src/app/(public)/informations/[slug]/page.tsx @@ -2,6 +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'; @@ -52,10 +53,13 @@ export default function ArticlePage() { {feed.image_url && (
- {feed.titre}
)} diff --git a/frontend/src/app/(public)/informations/page.tsx b/frontend/src/app/(public)/informations/page.tsx index 2242774..5d4bb4c 100644 --- a/frontend/src/app/(public)/informations/page.tsx +++ b/frontend/src/app/(public)/informations/page.tsx @@ -1,6 +1,7 @@ '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'; @@ -39,10 +40,13 @@ export default function InformationsPage() { {feed.image_url && (
- {feed.titre}
)} diff --git a/frontend/src/app/(public)/mentions-legales/page.tsx b/frontend/src/app/(public)/mentions-legales/page.tsx new file mode 100644 index 0000000..c9cb2ab --- /dev/null +++ b/frontend/src/app/(public)/mentions-legales/page.tsx @@ -0,0 +1,73 @@ +import Card from '@/components/ui/Card'; + +export default function MentionsLegalesPage() { + return ( +
+

+ Mentions légales +

+ +
+ +

+ Éditeur de l'application +

+

+ CESIZen est une application développée dans le cadre d'un projet + pédagogique au sein de{' '} + CESI École d'Ingénieurs. Cette application est + réalisée à but éducatif et n'a pas de vocation commerciale. +

+
+ + +

+ Hébergement +

+

+ 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. +

+
+ + +

+ Propriété intellectuelle +

+

+ 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. +

+
+ + +

+ Responsabilité +

+

+ 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é. +

+
+ + +

+ Contact +

+

+ Pour toute question relative aux mentions légales, vous pouvez nous + contacter via l'adresse suivante :{' '} + + contact@cesizen.fr + +

+
+
+
+ ); +} diff --git a/frontend/src/app/(public)/politique-de-confidentialite/page.tsx b/frontend/src/app/(public)/politique-de-confidentialite/page.tsx new file mode 100644 index 0000000..fbfc33d --- /dev/null +++ b/frontend/src/app/(public)/politique-de-confidentialite/page.tsx @@ -0,0 +1,105 @@ +import Card from '@/components/ui/Card'; + +export default function PolitiqueConfidentialitePage() { + return ( +
+

+ Politique de confidentialité +

+ +
+ +

+ Conformité RGPD +

+

+ 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é. +

+
+ + +

+ Données collectées +

+

+ Dans le cadre de l'utilisation de CESIZen, les données suivantes + sont collectées : +

+
    +
  • Adresse e-mail
  • +
  • Nom et prénom
  • +
  • Données relatives aux émotions enregistrées
  • +
+
+ + +

+ Finalité du traitement +

+

+ 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. +

+
+ + +

+ Durée de conservation +

+

+ 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. +

+
+ + +

+ Sécurité des données +

+

+ 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. +

+
+ + +

+ Vos droits +

+

+ Conformément au RGPD, vous disposez des droits suivants : +

+
    +
  • + Droit d'accès : obtenir une copie de vos + données personnelles +
  • +
  • + Droit de rectification : corriger des données + inexactes ou incomplètes +
  • +
  • + Droit de suppression : demander l'effacement + de vos données personnelles +
  • +
  • + Droit à la portabilité : récupérer vos données dans + un format structuré +
  • +
+

+ Pour exercer ces droits, contactez-nous à l'adresse :{' '} + + contact@cesizen.fr + +

+
+
+
+ ); +} diff --git a/frontend/src/components/features/EditSaisieModal.tsx b/frontend/src/components/features/EditSaisieModal.tsx new file mode 100644 index 0000000..405b5d1 --- /dev/null +++ b/frontend/src/components/features/EditSaisieModal.tsx @@ -0,0 +1,122 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import type { SaisieTracker } from '@/types'; +import { useUpdateSaisie } from '@/hooks/useTracker'; +import EmotionPicker from '@/components/features/EmotionPicker'; +import Slider from '@/components/ui/Slider'; +import Modal from '@/components/ui/Modal'; +import Button from '@/components/ui/Button'; +import toast from 'react-hot-toast'; + +interface EditSaisieModalProps { + saisie: SaisieTracker | null; + isOpen: boolean; + onClose: () => void; +} + +export default function EditSaisieModal({ saisie, isOpen, onClose }: EditSaisieModalProps) { + const updateSaisie = useUpdateSaisie(); + const [emotionId, setEmotionId] = useState(); + const [intensite, setIntensite] = useState(5); + const [note, setNote] = useState(''); + const [dateSaisie, setDateSaisie] = useState(''); + + useEffect(() => { + if (saisie) { + setEmotionId(saisie.emotion_id); + setIntensite(saisie.intensite); + setNote(saisie.note || ''); + setDateSaisie(saisie.date_saisie.split('T')[0]); + } + }, [saisie]); + + const handleSubmit = async () => { + if (!saisie || !emotionId) return; + + try { + await updateSaisie.mutateAsync({ + id: saisie.id, + emotion_id: emotionId, + intensite, + note: note.trim() || undefined, + date_saisie: dateSaisie, + }); + toast.success('Saisie modifiée !'); + onClose(); + } catch { + toast.error('Erreur lors de la modification'); + } + }; + + return ( + +
+ {/* Émotion */} +
+ + +
+ + {/* Intensité */} +
+ +
+ Faible + Modérée + Forte +
+
+ + {/* Date */} +
+ + 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" + /> +
+ + {/* Note */} +
+ +