6f5b8182ca
- mobile: externalise l'URL d'API via EXPO_PUBLIC_API_URL (repli localhost en dev) - backend: .env.example adapté au projet (PostgreSQL, JWT, FRONTEND_URL, Redis) - frontend/mobile: ajoute .env.example (+ exception gitignore front) - mobile: supprime les 4 `any` (helper getApiError, type Feed, glyphMap Ionicons) - backend: normalise le style PSR-12 via Pint (13 fichiers) Tests backend 15/15, mobile tsc 0, Pint pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
import { useState } from 'react';
|
|
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
|
import { Link, useRouter } from 'expo-router';
|
|
import { useAuthStore } from '@/lib/auth-store';
|
|
import { getApiError } from '@/lib/api';
|
|
import { Colors } from '@/lib/colors';
|
|
|
|
export default function LoginScreen() {
|
|
const router = useRouter();
|
|
const { login } = useAuthStore();
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleLogin = async () => {
|
|
if (!email || !password) {
|
|
Alert.alert('Erreur', 'Veuillez remplir tous les champs');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
await login(email, password);
|
|
router.replace('/(tabs)');
|
|
} catch (error) {
|
|
Alert.alert('Erreur', getApiError(error).message || 'Identifiants incorrects');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
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}>CESIZen</Text>
|
|
<Text style={styles.subtitle}>Votre compagnon de sante mentale</Text>
|
|
</View>
|
|
|
|
<View style={styles.form}>
|
|
<Text style={styles.title}>Connexion</Text>
|
|
|
|
<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"
|
|
autoCorrect={false}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.inputGroup}>
|
|
<Text style={styles.label}>Mot de passe</Text>
|
|
<TextInput
|
|
style={styles.input}
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
placeholder="Votre mot de passe"
|
|
secureTextEntry
|
|
/>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.button, loading && styles.buttonDisabled]}
|
|
onPress={handleLogin}
|
|
disabled={loading}
|
|
>
|
|
<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>
|
|
<TouchableOpacity>
|
|
<Text style={styles.link}>S'inscrire</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: 40 },
|
|
logo: { fontSize: 36, fontWeight: '800', color: Colors.black },
|
|
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
|
|
form: { backgroundColor: Colors.gray[50], borderRadius: 24, padding: 24 },
|
|
title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 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,
|
|
},
|
|
button: {
|
|
backgroundColor: Colors.primary,
|
|
borderRadius: 12,
|
|
paddingVertical: 16,
|
|
alignItems: 'center',
|
|
marginTop: 8,
|
|
},
|
|
buttonDisabled: { opacity: 0.6 },
|
|
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
|
|
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' },
|
|
});
|