fix: corrige les dépendances mobile et la qualité du code frontend

Mobile (Expo SDK 53) :
- supprime victory-native et react-native-svg (dépendances mortes, non
  importées) qui empêchaient l'installation (ETARGET sur svg@~16.0.0 +
  conflit de peer reanimated/skia)
- ajoute les peers manquants expo-constants et expo-linking (requis par
  expo-router)
- aligne expo-router, react-native et react-native-screens sur le SDK 53
- ajoute package-lock.json (installs reproductibles)
- expo-doctor 18/18, tsc sans erreur

Frontend (Next.js) :
- EmotionChart : remplace 4 `any` par les types Recharts (ValueType/NameType)
- EditSaisieModal : remplace le setState dans useEffect par l'ajustement
  d'état pendant le rendu (pattern recommandé par React)
- tsc, eslint et build de production sans erreur

Backend : 15 tests passent (Auth, Admin, Tracker, RGPD).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-16 17:19:07 +02:00
parent bfd35905c9
commit 2ec2a78469
23 changed files with 9905 additions and 60 deletions
+112
View File
@@ -0,0 +1,112 @@
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function ForgotPasswordScreen() {
const router = useRouter();
const [email, setEmail] = useState('');
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = () => {
setError(null);
if (!email.trim()) {
setError('Veuillez renseigner votre adresse email.');
return;
}
if (!EMAIL_REGEX.test(email)) {
setError('Format d\'email invalide.');
return;
}
setSubmitted(true);
};
if (submitted) {
return (
<View style={styles.container}>
<View style={styles.success}>
<View style={styles.successIcon}>
<Ionicons name="mail-open" size={32} color={Colors.secondary} />
</View>
<Text style={styles.successTitle}>Demande enregistrée</Text>
<Text style={styles.successText}>
Si l'adresse <Text style={styles.bold}>{email}</Text> est associée à un compte CESIZen, un email contenant les instructions de réinitialisation sera envoyé.
</Text>
<Text style={styles.successText}>
Si vous ne recevez rien sous 24h, contactez votre administrateur.
</Text>
<TouchableOpacity style={styles.button} onPress={() => router.replace('/auth/login')}>
<Text style={styles.buttonText}>Retour à la connexion</Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.logo}>Mot de passe oublié ?</Text>
<Text style={styles.subtitle}>
Entrez votre adresse email. Si elle est associée à un compte, vous recevrez les instructions pour réinitialiser votre mot de passe.
</Text>
</View>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={(v) => { setEmail(v); setError(null); }}
placeholder="votre@email.com"
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
<TouchableOpacity style={styles.button} onPress={handleSubmit}>
<Text style={styles.buttonText}>Envoyer les instructions</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Link href="/auth/login" asChild>
<TouchableOpacity>
<Text style={styles.link}>Retour à la connexion</Text>
</TouchableOpacity>
</Link>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
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 },
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 },
footer: { alignItems: 'center', marginTop: 20 },
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
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 },
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 },
});
+8 -1
View File
@@ -70,6 +70,12 @@ export default function LoginScreen() {
<Text style={styles.buttonText}>{loading ? 'Connexion...' : 'Se connecter'}</Text>
</TouchableOpacity>
<Link href="/auth/forgot-password" asChild>
<TouchableOpacity style={styles.forgotLink}>
<Text style={styles.link}>Mot de passe oublié ?</Text>
</TouchableOpacity>
</Link>
<View style={styles.footer}>
<Text style={styles.footerText}>Pas encore de compte ? </Text>
<Link href="/auth/register" asChild>
@@ -113,7 +119,8 @@ const styles = StyleSheet.create({
},
buttonDisabled: { opacity: 0.6 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
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' },
});
+62 -20
View File
@@ -1,9 +1,12 @@
import { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import { Link, useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { Colors } from '@/lib/colors';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function RegisterScreen() {
const router = useRouter();
const { register } = useAuthStore();
@@ -12,23 +15,43 @@ export default function RegisterScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [consentement, setConsentement] = useState(false);
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validate = (): boolean => {
const e: Record<string, string> = {};
if (!nom.trim()) e.nom = 'Le nom est obligatoire';
if (!prenom.trim()) e.prenom = 'Le prénom est obligatoire';
if (!email.trim()) e.email = 'L\'email est obligatoire';
else if (!EMAIL_REGEX.test(email)) e.email = 'Format d\'email invalide';
if (!password) e.password = 'Le mot de passe est obligatoire';
else if (password.length < 8) e.password = 'Le mot de passe doit faire au moins 8 caractères';
if (password !== passwordConfirmation) e.passwordConfirmation = 'Les mots de passe ne correspondent pas';
if (!consentement) e.consentement = 'Vous devez accepter le traitement de vos données (RGPD)';
setErrors(e);
return Object.keys(e).length === 0;
};
const handleRegister = async () => {
if (!nom || !prenom || !email || !password || !passwordConfirmation) {
Alert.alert('Erreur', 'Veuillez remplir tous les champs');
return;
}
if (password !== passwordConfirmation) {
Alert.alert('Erreur', 'Les mots de passe ne correspondent pas');
return;
}
if (!validate()) return;
setLoading(true);
try {
await register({ nom, prenom, email, password, password_confirmation: passwordConfirmation });
await register({
nom, prenom, email, password,
password_confirmation: passwordConfirmation,
consentement_rgpd: consentement,
});
router.replace('/(tabs)');
} catch (error: any) {
Alert.alert('Erreur', error.response?.data?.message || 'Erreur lors de l\'inscription');
const msg = error.response?.data?.message || 'Erreur lors de l\'inscription';
const apiErrors = error.response?.data?.errors;
if (apiErrors) {
const mapped: Record<string, string> = {};
Object.entries(apiErrors).forEach(([k, v]) => { mapped[k] = Array.isArray(v) ? v[0] as string : String(v); });
setErrors(mapped);
}
Alert.alert('Erreur', msg);
} finally {
setLoading(false);
}
@@ -39,7 +62,7 @@ export default function RegisterScreen() {
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<Text style={styles.logo}>CESIZen</Text>
<Text style={styles.subtitle}>Creez votre compte</Text>
<Text style={styles.subtitle}>Créez votre compte</Text>
</View>
<View style={styles.form}>
@@ -49,34 +72,50 @@ export default function RegisterScreen() {
<View style={[styles.inputGroup, { flex: 1, marginRight: 8 }]}>
<Text style={styles.label}>Nom</Text>
<TextInput style={styles.input} value={nom} onChangeText={setNom} placeholder="Dupont" />
{errors.nom && <Text style={styles.errorText}>{errors.nom}</Text>}
</View>
<View style={[styles.inputGroup, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>Prenom</Text>
<Text style={styles.label}>Prénom</Text>
<TextInput style={styles.input} value={prenom} onChangeText={setPrenom} placeholder="Jean" />
{errors.prenom && <Text style={styles.errorText}>{errors.prenom}</Text>}
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Email</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="votre@email.com" keyboardType="email-address" autoCapitalize="none" />
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="votre@email.com" keyboardType="email-address" autoCapitalize="none" autoCorrect={false} />
{errors.email && <Text style={styles.errorText}>{errors.email}</Text>}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe</Text>
<Text style={styles.label}>Mot de passe (min. 8 caractères)</Text>
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="Mot de passe" secureTextEntry />
{errors.password && <Text style={styles.errorText}>{errors.password}</Text>}
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Confirmer le mot de passe</Text>
<TextInput style={styles.input} value={passwordConfirmation} onChangeText={setPasswordConfirmation} placeholder="Confirmer" secureTextEntry />
{errors.passwordConfirmation && <Text style={styles.errorText}>{errors.passwordConfirmation}</Text>}
</View>
<TouchableOpacity style={styles.consentRow} onPress={() => setConsentement(!consentement)} activeOpacity={0.7}>
<View style={[styles.checkbox, consentement && styles.checkboxChecked]}>
{consentement && <Ionicons name="checkmark" size={16} color={Colors.white} />}
</View>
<Text style={styles.consentText}>
J'accepte le traitement de mes données de bien-être conformément à la <Text style={styles.consentLink}>politique de confidentialité</Text> (RGPD).
</Text>
</TouchableOpacity>
{errors.consentement && <Text style={styles.errorText}>{errors.consentement}</Text>}
{errors.consentement_rgpd && <Text style={styles.errorText}>{errors.consentement_rgpd}</Text>}
<TouchableOpacity style={[styles.button, loading && styles.buttonDisabled]} onPress={handleRegister} disabled={loading}>
<Text style={styles.buttonText}>{loading ? 'Inscription...' : 'S\'inscrire'}</Text>
</TouchableOpacity>
<View style={styles.footer}>
<Text style={styles.footerText}>Deja un compte ? </Text>
<Text style={styles.footerText}>Déjà un compte ? </Text>
<Link href="/auth/login" asChild>
<TouchableOpacity><Text style={styles.link}>Se connecter</Text></TouchableOpacity>
</Link>
@@ -98,11 +137,14 @@ const styles = StyleSheet.create({
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,
},
button: { backgroundColor: Colors.primary, borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 8 },
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 },
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 },
buttonDisabled: { opacity: 0.6 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },