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.
This commit is contained in:
Azmog
2026-04-15 16:09:20 +02:00
parent 88f5863cb5
commit bfd35905c9
60 changed files with 2437 additions and 42 deletions
@@ -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);
}
@@ -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';
@@ -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);
}
@@ -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);
}
+4 -2
View File
@@ -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 ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
+6 -4
View File
@@ -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<string, string[]> }>;
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 ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
@@ -181,8 +183,8 @@ export default function InscriptionPage() {
/>
<label htmlFor="consentement_rgpd" className="text-sm text-gray-600 dark:text-gray-400">
J&apos;accepte le traitement de mes données personnelles conformément à la{' '}
<Link href="/informations" className="text-malachite-600 dark:text-malachite-400 hover:underline">
politique de confidentialite
<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>
@@ -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';
+12 -1
View File
@@ -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<SaisieTracker | null>(null);
const { data: saisies, isLoading } = useSaisies(filters);
const { data: emotions } = useEmotions();
@@ -134,9 +137,17 @@ export default function JournalPage() {
) : (
<TrackerTimeline
saisies={saisies || []}
onEdit={(saisie) => setEditingSaisie(saisie)}
onDelete={handleDelete}
/>
)}
{/* Modale d'édition */}
<EditSaisieModal
saisie={editingSaisie}
isOpen={!!editingSaisie}
onClose={() => setEditingSaisie(null)}
/>
</div>
);
}
+9 -6
View File
@@ -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);
}
@@ -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 (
<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&apos;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&apos;hésitez pas à
contacter l&apos;un de ces services. Vous n&apos;ê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>
<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&apos;urgence vitale</strong>, appelez immédiatement le{' '}
<strong>15</strong> (SAMU) ou le <strong>112</strong> (numéro d&apos;urgence
européen).
</p>
</Card>
</div>
);
}
@@ -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 && (
<div className="relative w-full h-64 sm:h-80 rounded-2xl overflow-hidden bg-gray-100 dark:bg-gray-800 mb-8">
<img
<Image
src={feed.image_url}
alt={feed.titre}
width={800}
height={400}
className="w-full h-full object-cover"
unoptimized
/>
</div>
)}
@@ -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() {
<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">
<img
<Image
src={feed.image_url}
alt={feed.titre}
width={800}
height={400}
className="w-full h-full object-cover"
unoptimized
/>
</div>
)}
@@ -0,0 +1,73 @@
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="space-y-6">
<Card>
<h2 className="font-display text-xl font-semibold text-gray-900 dark:text-white mb-3">
Éditeur de l&apos;application
</h2>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
CESIZen est une application développée dans le cadre d&apos;un projet
pédagogique au sein de{' '}
<strong>CESI École d&apos;Ingénieurs</strong>. Cette application est
réalisée à but éducatif et n&apos;a pas de vocation commerciale.
</p>
</Card>
<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&apos;application est hébergée dans le cadre de l&apos;infrastructure
pédagogique de CESI. Les données sont stockées sur des serveurs
sécurisés situés en France.
</p>
</Card>
<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&apos;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>
<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&apos;urgence ou un professionnel de
santé.
</p>
</Card>
<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&apos;adresse suivante :{' '}
<span className="text-malachite-600 dark:text-malachite-400">
contact@cesizen.fr
</span>
</p>
</Card>
</div>
</div>
);
}
@@ -0,0 +1,105 @@
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="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&apos;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>
<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&apos;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>
<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>
<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&apos;utilisation du compte. En cas de suppression du compte, les
données sont effacées dans un délai de 30 jours.
</p>
</Card>
<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&apos;accès aux
données est strictement limité aux personnes habilitées.
</p>
</Card>
<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&apos;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&apos;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&apos;adresse :{' '}
<span className="text-malachite-600 dark:text-malachite-400">
contact@cesizen.fr
</span>
</p>
</Card>
</div>
</div>
);
}
@@ -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<number | undefined>();
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 (
<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>
{/* 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>
</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>
{/* 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>
{/* Actions */}
<div className="flex justify-end gap-3 pt-2">
<Button variant="ghost" onClick={onClose}>
Annuler
</Button>
<Button
onClick={handleSubmit}
loading={updateSaisie.isPending}
disabled={!emotionId}
>
Enregistrer
</Button>
</div>
</div>
</Modal>
);
}
+1 -1
View File
@@ -38,7 +38,7 @@ interface AuthState {
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
(set) => ({
user: null,
token: null,
isAuthenticated: false,