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:
@@ -38,9 +38,17 @@ export default function TabLayout() {
|
||||
name="info"
|
||||
options={{
|
||||
title: 'Infos',
|
||||
headerShown: false,
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="information-circle" size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="urgence"
|
||||
options={{
|
||||
title: 'Urgence',
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="call" size={size} color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="profile"
|
||||
options={{
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
|
||||
import { useLocalSearchParams, Stack } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFeed } from '@/hooks/useFeeds';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export default function FeedDetailScreen() {
|
||||
const { slug } = useLocalSearchParams<{ slug: string }>();
|
||||
const { data: feed, isLoading, isError } = useFeed(slug);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.secondary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !feed) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color={Colors.gray[400]} />
|
||||
<Text style={styles.errorText}>Article introuvable</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<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} />
|
||||
<Text style={styles.badgeText}>Information santé mentale</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>{feed.titre}</Text>
|
||||
|
||||
<Text style={styles.date}>
|
||||
Publié le {new Date(feed.created_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.body}>{texteNettoye}</Text>
|
||||
</ScrollView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
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' },
|
||||
errorText: { marginTop: 12, fontSize: 16, color: Colors.gray[400] },
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export default function InfoLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: Colors.white },
|
||||
headerTitleStyle: { fontWeight: '700', color: Colors.black },
|
||||
headerTintColor: Colors.secondary,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ title: 'Infos' }} />
|
||||
<Stack.Screen name="[slug]" options={{ title: '', headerBackTitle: 'Retour' }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useFeeds } from '@/hooks/useFeeds';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
export default function InfoScreen() {
|
||||
export default function InfoListScreen() {
|
||||
const { data: feeds, isLoading } = useFeeds();
|
||||
const router = useRouter();
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<TouchableOpacity style={styles.card}>
|
||||
<TouchableOpacity
|
||||
style={styles.card}
|
||||
onPress={() => router.push(`/(tabs)/info/${item.slug}`)}
|
||||
>
|
||||
<View style={styles.cardIcon}>
|
||||
<Ionicons name="document-text" size={24} color={Colors.secondary} />
|
||||
</View>
|
||||
@@ -33,8 +38,8 @@ export default function InfoScreen() {
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Sante mentale</Text>
|
||||
<Text style={styles.headerSubtitle}>Ressources et informations pour votre bien-etre</Text>
|
||||
<Text style={styles.headerTitle}>Santé mentale</Text>
|
||||
<Text style={styles.headerSubtitle}>Ressources et informations pour votre bien-être</Text>
|
||||
</View>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
@@ -0,0 +1,125 @@
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Linking, Alert } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Colors } from '@/lib/colors';
|
||||
|
||||
type Contact = {
|
||||
nom: string;
|
||||
numero: string;
|
||||
description: string;
|
||||
disponibilite: string;
|
||||
};
|
||||
|
||||
const CONTACTS: Contact[] = [
|
||||
{
|
||||
nom: 'Prévention du suicide',
|
||||
numero: '3114',
|
||||
description: 'Numéro national gratuit et confidentiel pour les personnes en détresse psychologique et leur entourage.',
|
||||
disponibilite: '24h/24 — 7j/7',
|
||||
},
|
||||
{
|
||||
nom: 'SOS Amitié',
|
||||
numero: '0972394050',
|
||||
description: 'Écoute et soutien pour les personnes en souffrance, solitude ou détresse morale.',
|
||||
disponibilite: '24h/24',
|
||||
},
|
||||
{
|
||||
nom: 'Fil Santé Jeunes',
|
||||
numero: '0800235236',
|
||||
description: 'Ligne d\'écoute gratuite et anonyme pour les 12-25 ans.',
|
||||
disponibilite: 'Lun-Dim, 9h-23h',
|
||||
},
|
||||
{
|
||||
nom: 'SAMU',
|
||||
numero: '15',
|
||||
description: 'Service d\'aide médicale urgente. En cas d\'urgence vitale.',
|
||||
disponibilite: '24h/24 — 7j/7',
|
||||
},
|
||||
{
|
||||
nom: 'SOS Médecins',
|
||||
numero: '3624',
|
||||
description: 'Consultations médicales à domicile.',
|
||||
disponibilite: '24h/24 — 7j/7',
|
||||
},
|
||||
{
|
||||
nom: 'Numéro européen d\'urgence',
|
||||
numero: '112',
|
||||
description: 'Police, pompiers, SAMU. À utiliser en cas d\'urgence vitale.',
|
||||
disponibilite: '24h/24 — 7j/7',
|
||||
},
|
||||
];
|
||||
|
||||
function formatNumero(n: string): string {
|
||||
if (n.length <= 4) return n;
|
||||
return n.replace(/(\d{2})(?=\d)/g, '$1 ').trim();
|
||||
}
|
||||
|
||||
export default function UrgenceScreen() {
|
||||
const appeler = async (numero: string, nom: string) => {
|
||||
const url = `tel:${numero}`;
|
||||
const supported = await Linking.canOpenURL(url);
|
||||
if (supported) {
|
||||
Linking.openURL(url);
|
||||
} else {
|
||||
Alert.alert('Impossible d\'appeler', `Composez manuellement le ${formatNumero(numero)} pour joindre ${nom}.`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconBadge}>
|
||||
<Ionicons name="heart" size={20} color="#DC2626" />
|
||||
</View>
|
||||
<Text style={styles.headerTitle}>Vous n'êtes pas seul(e)</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
Si vous ou un proche traversez une période difficile, ces services sont là pour vous écouter.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{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" />
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardName}>{c.nom}</Text>
|
||||
<Text style={styles.cardNumber}>{formatNumero(c.numero)}</Text>
|
||||
<Text style={styles.cardDescription}>{c.description}</Text>
|
||||
<Text style={styles.cardAvailability}>{c.disponibilite}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
<View style={styles.warning}>
|
||||
<Ionicons name="warning" size={18} color="#DC2626" />
|
||||
<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>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
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 },
|
||||
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],
|
||||
},
|
||||
cardIcon: { width: 44, height: 44, borderRadius: 12, backgroundColor: '#FEE2E2', 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 },
|
||||
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 },
|
||||
bold: { fontWeight: '800' },
|
||||
});
|
||||
@@ -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 },
|
||||
});
|
||||
@@ -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' },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user