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
+53
View File
@@ -0,0 +1,53 @@
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors.secondary,
tabBarInactiveTintColor: Colors.gray[400],
tabBarStyle: {
backgroundColor: Colors.white,
borderTopColor: Colors.gray[200],
paddingBottom: 8,
paddingTop: 8,
height: 65,
},
headerStyle: { backgroundColor: Colors.white },
headerTitleStyle: { fontWeight: '700', color: Colors.black },
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Accueil',
tabBarIcon: ({ color, size }) => <Ionicons name="home" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="journal"
options={{
title: 'Journal',
headerShown: false,
tabBarIcon: ({ color, size }) => <Ionicons name="book" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="info"
options={{
title: 'Infos',
tabBarIcon: ({ color, size }) => <Ionicons name="information-circle" size={size} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profil',
tabBarIcon: ({ color, size }) => <Ionicons name="person" size={size} color={color} />,
}}
/>
</Tabs>
);
}
+121
View File
@@ -0,0 +1,121 @@
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { useRapport } from '@/hooks/useRapport';
import { useSaisies } from '@/hooks/useTracker';
import { Colors } from '@/lib/colors';
export default function DashboardScreen() {
const router = useRouter();
const { user } = useAuthStore();
const { data: rapport } = useRapport('month');
const { data: saisies } = useSaisies();
const recentSaisies = (saisies || []).slice(0, 3);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.greeting}>Bonjour, {user?.prenom} 👋</Text>
<Text style={styles.subtitle}>Comment allez-vous aujourd'hui ?</Text>
{/* Actions rapides */}
<View style={styles.actionsRow}>
<TouchableOpacity style={styles.actionCard} onPress={() => router.push('/(tabs)/journal/new')}>
<View style={[styles.actionIcon, { backgroundColor: `${Colors.primary}30` }]}>
<Ionicons name="add-circle" size={28} color={Colors.primary} />
</View>
<Text style={styles.actionText}>Nouvelle saisie</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionCard} onPress={() => router.push('/(tabs)/journal/reports')}>
<View style={[styles.actionIcon, { backgroundColor: `${Colors.secondary}30` }]}>
<Ionicons name="bar-chart" size={28} color={Colors.secondary} />
</View>
<Text style={styles.actionText}>Mes rapports</Text>
</TouchableOpacity>
</View>
{/* Stats du mois */}
{rapport && (
<View style={styles.statsContainer}>
<Text style={styles.sectionTitle}>Ce mois-ci</Text>
<View style={styles.statsRow}>
<View style={styles.statCard}>
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensite moy.</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel}>{rapport.stats.emotion_dominante?.nom || 'Aucune'}</Text>
</View>
</View>
</View>
)}
{/* Dernieres saisies */}
{recentSaisies.length > 0 && (
<View style={styles.recentContainer}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Dernieres emotions</Text>
<TouchableOpacity onPress={() => router.push('/(tabs)/journal')}>
<Text style={styles.seeAll}>Tout voir</Text>
</TouchableOpacity>
</View>
{recentSaisies.map((saisie) => (
<View key={saisie.id} style={styles.saisieItem}>
<View style={[styles.emotionIcon, { backgroundColor: `${saisie.emotion?.couleur}20` }]}>
<Text style={styles.emotionEmoji}>{saisie.emotion?.icone || '🔵'}</Text>
</View>
<View style={styles.saisieContent}>
<Text style={styles.saisieEmotion}>{saisie.emotion?.nom}</Text>
<Text style={styles.saisieDate}>
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensite: {saisie.intensite}/10
</Text>
</View>
</View>
))}
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20 },
greeting: { fontSize: 28, fontWeight: '800', color: Colors.black, marginTop: 8 },
subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 4, marginBottom: 24 },
actionsRow: { flexDirection: 'row', gap: 12, marginBottom: 24 },
actionCard: {
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 20,
alignItems: 'center', gap: 12, borderWidth: 1, borderColor: Colors.gray[100],
},
actionIcon: { width: 56, height: 56, borderRadius: 16, justifyContent: 'center', alignItems: 'center' },
actionText: { fontSize: 14, fontWeight: '600', color: Colors.gray[700] },
statsContainer: { marginBottom: 24 },
sectionTitle: { fontSize: 18, fontWeight: '700', color: Colors.black, marginBottom: 12 },
statsRow: { flexDirection: 'row', gap: 12 },
statCard: {
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 16,
alignItems: 'center', borderWidth: 1, borderColor: Colors.gray[100],
},
statValue: { fontSize: 24, fontWeight: '800', color: Colors.black },
statLabel: { fontSize: 12, color: Colors.gray[500], marginTop: 4 },
recentContainer: { marginBottom: 24 },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 },
seeAll: { fontSize: 14, fontWeight: '600', color: Colors.secondary },
saisieItem: {
flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14,
backgroundColor: Colors.gray[50], borderRadius: 14, marginBottom: 8,
borderWidth: 1, borderColor: Colors.gray[100],
},
emotionIcon: { width: 44, height: 44, borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
emotionEmoji: { fontSize: 22 },
saisieContent: { flex: 1 },
saisieEmotion: { fontSize: 15, fontWeight: '600', color: Colors.black },
saisieDate: { fontSize: 13, color: Colors.gray[500], marginTop: 2 },
});
+75
View File
@@ -0,0 +1,75 @@
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useFeeds } from '@/hooks/useFeeds';
import { Colors } from '@/lib/colors';
export default function InfoScreen() {
const { data: feeds, isLoading } = useFeeds();
const renderItem = ({ item }: { item: any }) => (
<TouchableOpacity style={styles.card}>
<View style={styles.cardIcon}>
<Ionicons name="document-text" size={24} color={Colors.secondary} />
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{item.titre}</Text>
<Text style={styles.cardExcerpt} numberOfLines={2}>
{item.contenu?.replace(/<[^>]*>/g, '').substring(0, 120)}...
</Text>
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleDateString('fr-FR')}
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={Colors.gray[300]} />
</TouchableOpacity>
);
return (
<View style={styles.container}>
<FlatList
data={feeds || []}
keyExtractor={(item) => String(item.id)}
renderItem={renderItem}
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>
</View>
}
ListEmptyComponent={
isLoading ? (
<View style={styles.loading}><Text style={styles.loadingText}>Chargement...</Text></View>
) : (
<View style={styles.empty}>
<Ionicons name="newspaper-outline" size={48} color={Colors.gray[300]} />
<Text style={styles.emptyText}>Aucun article disponible</Text>
</View>
)
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
listContent: { padding: 16 },
header: { marginBottom: 20 },
headerTitle: { fontSize: 14, fontWeight: '700', color: Colors.secondary, textTransform: 'uppercase', letterSpacing: 1 },
headerSubtitle: { fontSize: 15, color: Colors.gray[500], marginTop: 4 },
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: 48, height: 48, borderRadius: 14, backgroundColor: `${Colors.secondary}15`, justifyContent: 'center', alignItems: 'center' },
cardContent: { flex: 1 },
cardTitle: { fontSize: 15, fontWeight: '700', color: Colors.black, marginBottom: 4 },
cardExcerpt: { fontSize: 13, color: Colors.gray[500], lineHeight: 18 },
cardDate: { fontSize: 11, color: Colors.gray[400], marginTop: 4 },
loading: { padding: 40, alignItems: 'center' },
loadingText: { color: Colors.gray[400] },
empty: { alignItems: 'center', paddingTop: 60 },
emptyText: { fontSize: 16, color: Colors.gray[400], marginTop: 12 },
});
+18
View File
@@ -0,0 +1,18 @@
import { Stack } from 'expo-router';
import { Colors } from '@/lib/colors';
export default function JournalLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: Colors.white },
headerTitleStyle: { fontWeight: '700', color: Colors.black },
}}
>
<Stack.Screen name="index" options={{ title: 'Mon journal' }} />
<Stack.Screen name="new" options={{ title: 'Nouvelle saisie', presentation: 'modal' }} />
<Stack.Screen name="edit" options={{ title: 'Modifier la saisie', presentation: 'modal' }} />
<Stack.Screen name="reports" options={{ title: 'Mes rapports' }} />
</Stack>
);
}
+162
View File
@@ -0,0 +1,162 @@
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSaisies, useUpdateSaisie } from '@/hooks/useTracker';
import EmotionPicker from '@/components/EmotionPicker';
import { Colors } from '@/lib/colors';
export default function EditEntryScreen() {
const router = useRouter();
const { id } = useLocalSearchParams<{ id: string }>();
const { data: saisies } = useSaisies();
const updateSaisie = useUpdateSaisie();
const saisie = saisies?.find((s) => s.id === Number(id));
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 (!emotionId || !id) return;
try {
await updateSaisie.mutateAsync({
id: Number(id),
emotion_id: emotionId,
intensite,
note: note.trim() || undefined,
date_saisie: dateSaisie,
});
Alert.alert('Succes', 'Saisie modifiee !', [
{ text: 'OK', onPress: () => router.back() },
]);
} catch {
Alert.alert('Erreur', 'Impossible de modifier la saisie');
}
};
if (!saisie) {
return (
<View style={styles.loading}>
<Text style={styles.loadingText}>Chargement...</Text>
</View>
);
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Modifier la saisie</Text>
{/* Emotion */}
<View style={styles.section}>
<Text style={styles.label}>Emotion</Text>
<EmotionPicker value={emotionId} onChange={setEmotionId} />
</View>
{/* Intensite */}
<View style={styles.section}>
<Text style={styles.label}>Intensite</Text>
<View style={styles.intensityContainer}>
<Text style={styles.intensityValue}>{intensite}</Text>
<Text style={styles.intensityUnit}>/10</Text>
</View>
<View style={styles.sliderButtons}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
<TouchableOpacity
key={v}
style={[styles.sliderDot, intensite === v && styles.sliderDotActive]}
onPress={() => setIntensite(v)}
>
<Text style={[styles.sliderDotText, intensite === v && styles.sliderDotTextActive]}>{v}</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.sliderLabels}>
<Text style={styles.sliderLabel}>Faible</Text>
<Text style={styles.sliderLabel}>Forte</Text>
</View>
</View>
{/* Note */}
<View style={styles.section}>
<Text style={styles.label}>Note (optionnel)</Text>
<TextInput
style={styles.textArea}
value={note}
onChangeText={setNote}
placeholder="Decrivez ce que vous ressentez..."
multiline
numberOfLines={4}
textAlignVertical="top"
/>
</View>
{/* Actions */}
<View style={styles.actionsRow}>
<TouchableOpacity style={styles.cancelBtn} onPress={() => router.back()}>
<Text style={styles.cancelBtnText}>Annuler</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitBtn, updateSaisie.isPending && styles.btnDisabled]}
onPress={handleSubmit}
disabled={updateSaisie.isPending || !emotionId}
>
<Ionicons name="checkmark" size={20} color={Colors.black} />
<Text style={styles.submitBtnText}>
{updateSaisie.isPending ? 'Envoi...' : 'Enregistrer'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
loadingText: { color: Colors.gray[400], fontSize: 16 },
title: { fontSize: 24, fontWeight: '800', color: Colors.black, marginBottom: 24 },
section: { marginBottom: 24 },
label: { fontSize: 15, fontWeight: '700', color: Colors.gray[700], marginBottom: 10 },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 16 },
intensityValue: { fontSize: 48, fontWeight: '800', color: Colors.black },
intensityUnit: { fontSize: 20, color: Colors.gray[400], marginLeft: 4 },
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 4 },
sliderDot: {
width: 28, height: 28, borderRadius: 14,
backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center',
},
sliderDotActive: { backgroundColor: Colors.primary },
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
sliderDotTextActive: { color: Colors.black },
sliderLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 },
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
textArea: {
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black, minHeight: 100,
},
actionsRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8, gap: 12 },
cancelBtn: {
flex: 1, paddingVertical: 16, borderRadius: 14,
backgroundColor: Colors.gray[100], alignItems: 'center',
},
cancelBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
submitBtn: {
flex: 1, flexDirection: 'row', gap: 8, paddingVertical: 16, borderRadius: 14,
backgroundColor: Colors.secondary, alignItems: 'center', justifyContent: 'center',
},
submitBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
btnDisabled: { opacity: 0.5 },
});
+121
View File
@@ -0,0 +1,121 @@
import { useState, useCallback } from 'react';
import { View, Text, StyleSheet, FlatList, TouchableOpacity, Alert, RefreshControl } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSaisies, useDeleteSaisie } from '@/hooks/useTracker';
import { Colors } from '@/lib/colors';
import type { SaisieTracker } from '@/types';
export default function JournalScreen() {
const router = useRouter();
const { data: saisies, isLoading, refetch } = useSaisies();
const deleteSaisie = useDeleteSaisie();
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
}, [refetch]);
const handleDelete = (id: number) => {
Alert.alert('Supprimer', 'Supprimer cette saisie ?', [
{ text: 'Annuler', style: 'cancel' },
{
text: 'Supprimer',
style: 'destructive',
onPress: async () => {
try {
await deleteSaisie.mutateAsync(id);
} catch {
Alert.alert('Erreur', 'Impossible de supprimer');
}
},
},
]);
};
const renderItem = ({ item }: { item: SaisieTracker }) => (
<View style={styles.saisieCard}>
<View style={[styles.emotionIcon, { backgroundColor: `${item.emotion?.couleur}20` }]}>
<Text style={styles.emotionEmoji}>{item.emotion?.icone || '🔵'}</Text>
</View>
<View style={styles.saisieContent}>
<View style={styles.saisieHeader}>
<View style={[styles.badge, { backgroundColor: `${item.emotion?.couleur}20` }]}>
<Text style={[styles.badgeText, { color: item.emotion?.couleur }]}>{item.emotion?.nom}</Text>
</View>
<Text style={styles.intensityText}>Intensite: {item.intensite}/10</Text>
</View>
<View style={styles.intensityBar}>
<View style={[styles.intensityFill, { width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary }]} />
</View>
{item.note && <Text style={styles.note} numberOfLines={2}>{item.note}</Text>}
<Text style={styles.date}>{new Date(item.date_saisie).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}</Text>
</View>
<View style={styles.actions}>
<TouchableOpacity style={styles.actionBtn} onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}>
<Ionicons name="create-outline" size={18} color={Colors.gray[400]} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item.id)}>
<Ionicons name="trash-outline" size={18} color={Colors.gray[400]} />
</TouchableOpacity>
</View>
</View>
);
return (
<View style={styles.container}>
<FlatList
data={saisies || []}
keyExtractor={(item) => String(item.id)}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary} />}
ListEmptyComponent={
<View style={styles.empty}>
<Ionicons name="book-outline" size={48} color={Colors.gray[300]} />
<Text style={styles.emptyTitle}>Aucune saisie</Text>
<Text style={styles.emptyText}>Commencez par enregistrer votre premiere emotion</Text>
</View>
}
/>
<TouchableOpacity style={styles.fab} onPress={() => router.push('/(tabs)/journal/new')}>
<Ionicons name="add" size={28} color={Colors.black} />
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
listContent: { padding: 16, paddingBottom: 80 },
saisieCard: {
flexDirection: 'row', alignItems: 'flex-start', gap: 12, padding: 16,
backgroundColor: Colors.gray[50], borderRadius: 16, marginBottom: 10,
borderWidth: 1, borderColor: Colors.gray[100],
},
emotionIcon: { width: 48, height: 48, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
emotionEmoji: { fontSize: 24 },
saisieContent: { flex: 1 },
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 6 },
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 8 },
badgeText: { fontSize: 13, fontWeight: '600' },
intensityText: { fontSize: 12, color: Colors.gray[400] },
intensityBar: { height: 4, backgroundColor: Colors.gray[200], borderRadius: 2, marginBottom: 6 },
intensityFill: { height: 4, borderRadius: 2 },
note: { fontSize: 13, color: Colors.gray[600], marginBottom: 4 },
date: { fontSize: 12, color: Colors.gray[400] },
actions: { gap: 4 },
actionBtn: { padding: 6, borderRadius: 8 },
empty: { alignItems: 'center', paddingTop: 80 },
emptyTitle: { fontSize: 18, fontWeight: '600', color: Colors.gray[400], marginTop: 12 },
emptyText: { fontSize: 14, color: Colors.gray[400], marginTop: 4 },
fab: {
position: 'absolute', right: 20, bottom: 20,
width: 56, height: 56, borderRadius: 28,
backgroundColor: Colors.primary, justifyContent: 'center', alignItems: 'center',
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8,
elevation: 6,
},
});
+184
View File
@@ -0,0 +1,184 @@
import { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useCreateSaisie } from '@/hooks/useTracker';
import EmotionPicker from '@/components/EmotionPicker';
import { Colors } from '@/lib/colors';
export default function NewEntryScreen() {
const router = useRouter();
const createSaisie = useCreateSaisie();
const [step, setStep] = useState(1);
const [emotionId, setEmotionId] = useState<number | undefined>();
const [intensite, setIntensite] = useState(5);
const [note, setNote] = useState('');
const handleSubmit = async () => {
if (!emotionId) return;
try {
await createSaisie.mutateAsync({
emotion_id: emotionId,
intensite,
note: note.trim() || undefined,
date_saisie: new Date().toISOString().split('T')[0],
});
Alert.alert('Succes', 'Saisie enregistree !', [
{ text: 'OK', onPress: () => router.back() },
]);
} catch {
Alert.alert('Erreur', 'Impossible d\'enregistrer la saisie');
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Stepper */}
<View style={styles.stepper}>
{[1, 2, 3].map((s) => (
<View key={s} style={styles.stepRow}>
<View style={[styles.stepCircle, step >= s && styles.stepActive]}>
{step > s ? (
<Ionicons name="checkmark" size={16} color={Colors.white} />
) : (
<Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text>
)}
</View>
{s < 3 && <View style={[styles.stepLine, step > s && styles.stepLineActive]} />}
</View>
))}
</View>
{/* Step 1: Emotion */}
{step === 1 && (
<View>
<Text style={styles.stepTitle}>Comment vous sentez-vous ?</Text>
<EmotionPicker value={emotionId} onChange={setEmotionId} />
<TouchableOpacity
style={[styles.nextBtn, !emotionId && styles.btnDisabled]}
onPress={() => emotionId && setStep(2)}
disabled={!emotionId}
>
<Text style={styles.nextBtnText}>Suivant</Text>
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
</TouchableOpacity>
</View>
)}
{/* Step 2: Intensity */}
{step === 2 && (
<View>
<Text style={styles.stepTitle}>A quel point ressentez-vous cette emotion ?</Text>
<View style={styles.intensityContainer}>
<Text style={styles.intensityValue}>{intensite}</Text>
<Text style={styles.intensityLabel}>/10</Text>
</View>
<View style={styles.sliderContainer}>
<Text style={styles.sliderLabel}>Faible</Text>
<View style={styles.sliderWrapper}>
<View style={styles.nativeSlider}>
<View style={styles.sliderTrack}>
<View style={[styles.sliderFill, { width: `${(intensite - 1) * 11.11}%` }]} />
</View>
<View style={styles.sliderButtons}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
<TouchableOpacity
key={v}
style={[styles.sliderDot, intensite === v && styles.sliderDotActive]}
onPress={() => setIntensite(v)}
>
<Text style={[styles.sliderDotText, intensite === v && styles.sliderDotTextActive]}>{v}</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
<Text style={styles.sliderLabel}>Forte</Text>
</View>
<View style={styles.navRow}>
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(1)}>
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
<Text style={styles.backBtnText}>Retour</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.nextBtn} onPress={() => setStep(3)}>
<Text style={styles.nextBtnText}>Suivant</Text>
<Ionicons name="arrow-forward" size={20} color={Colors.black} />
</TouchableOpacity>
</View>
</View>
)}
{/* Step 3: Note */}
{step === 3 && (
<View>
<Text style={styles.stepTitle}>Souhaitez-vous ajouter un commentaire ?</Text>
<TextInput
style={styles.textArea}
value={note}
onChangeText={setNote}
placeholder="Decrivez ce que vous ressentez, ce qui a declenche cette emotion..."
multiline
numberOfLines={5}
textAlignVertical="top"
/>
<View style={styles.navRow}>
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(2)}>
<Ionicons name="arrow-back" size={20} color={Colors.gray[600]} />
<Text style={styles.backBtnText}>Retour</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitBtn, createSaisie.isPending && styles.btnDisabled]}
onPress={handleSubmit}
disabled={createSaisie.isPending}
>
<Ionicons name="checkmark" size={20} color={Colors.black} />
<Text style={styles.nextBtnText}>{createSaisie.isPending ? 'Envoi...' : 'Enregistrer'}</Text>
</TouchableOpacity>
</View>
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: 32 },
stepRow: { flexDirection: 'row', alignItems: 'center' },
stepCircle: {
width: 32, height: 32, borderRadius: 16, backgroundColor: Colors.gray[200],
justifyContent: 'center', alignItems: 'center',
},
stepActive: { backgroundColor: Colors.secondary },
stepNumber: { fontSize: 14, fontWeight: '700', color: Colors.gray[500] },
stepNumberActive: { color: Colors.white },
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: 4 },
stepLineActive: { backgroundColor: Colors.secondary },
stepTitle: { fontSize: 20, fontWeight: '700', color: Colors.black, marginBottom: 20 },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: 24 },
intensityValue: { fontSize: 64, fontWeight: '800', color: Colors.black },
intensityLabel: { fontSize: 24, color: Colors.gray[400], marginLeft: 4 },
sliderContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 32 },
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
sliderWrapper: { flex: 1 },
nativeSlider: {},
sliderTrack: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 8 },
sliderFill: { height: 6, backgroundColor: Colors.primary, borderRadius: 3 },
sliderButtons: { flexDirection: 'row', justifyContent: 'space-between' },
sliderDot: { width: 28, height: 28, borderRadius: 14, backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center' },
sliderDotActive: { backgroundColor: Colors.primary },
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
sliderDotTextActive: { color: Colors.black },
textArea: {
backgroundColor: Colors.gray[50], borderWidth: 1, borderColor: Colors.gray[200],
borderRadius: 16, padding: 16, fontSize: 15, color: Colors.black,
minHeight: 120, marginBottom: 24,
},
navRow: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 16 },
backBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 20, borderRadius: 12, backgroundColor: Colors.gray[100] },
backBtnText: { fontSize: 15, fontWeight: '600', color: Colors.gray[600] },
nextBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.primary },
submitBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 12, backgroundColor: Colors.secondary },
nextBtnText: { fontSize: 15, fontWeight: '700', color: Colors.black },
btnDisabled: { opacity: 0.5 },
});
+134
View File
@@ -0,0 +1,134 @@
import { useState } from 'react';
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useRapport } from '@/hooks/useRapport';
import { Colors } from '@/lib/colors';
type Period = 'week' | 'month' | 'quarter' | 'year';
const PERIODS: { value: Period; label: string }[] = [
{ value: 'week', label: 'Semaine' },
{ value: 'month', label: 'Mois' },
{ value: 'quarter', label: 'Trimestre' },
{ value: 'year', label: 'Annee' },
];
export default function ReportsScreen() {
const [period, setPeriod] = useState<Period>('month');
const { data: rapport, isLoading } = useRapport(period);
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Period selector */}
<View style={styles.periodRow}>
{PERIODS.map((p) => (
<TouchableOpacity
key={p.value}
style={[styles.periodBtn, period === p.value && styles.periodBtnActive]}
onPress={() => setPeriod(p.value)}
>
<Text style={[styles.periodText, period === p.value && styles.periodTextActive]}>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
{isLoading ? (
<View style={styles.loading}>
<Text style={styles.loadingText}>Chargement...</Text>
</View>
) : rapport ? (
<>
{/* Stats cards */}
<View style={styles.statsRow}>
<View style={styles.statCard}>
<Ionicons name="bar-chart" size={20} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
<View style={styles.statCard}>
<Ionicons name="trending-up" size={20} color={Colors.secondary} />
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensite moy.</Text>
</View>
<View style={styles.statCard}>
<Ionicons name="heart" size={20} color="#FF69B4" />
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel}>{rapport.stats.emotion_dominante?.nom || 'Aucune'}</Text>
</View>
</View>
{/* Period info */}
<View style={styles.periodInfo}>
<Text style={styles.periodInfoText}>
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au {new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
</Text>
</View>
{/* Repartition */}
{rapport.repartition.length > 0 && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Repartition des emotions</Text>
{rapport.repartition.map((item) => (
<View key={item.nom} style={styles.repartitionItem}>
<View style={styles.repartitionHeader}>
<View style={[styles.colorDot, { backgroundColor: item.couleur }]} />
<Text style={styles.repartitionName}>{item.nom}</Text>
<Text style={styles.repartitionPct}>{item.pourcentage}%</Text>
</View>
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${item.pourcentage}%`, backgroundColor: item.couleur }]} />
</View>
<View style={styles.repartitionFooter}>
<Text style={styles.repartitionDetail}>{item.count} saisies</Text>
<Text style={styles.repartitionDetail}>Moy: {item.intensite_moyenne.toFixed(1)}/10</Text>
</View>
</View>
))}
</View>
)}
</>
) : (
<View style={styles.empty}>
<Ionicons name="bar-chart-outline" size={48} color={Colors.gray[300]} />
<Text style={styles.emptyText}>Pas de donnees pour cette periode</Text>
</View>
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 12, padding: 4, marginBottom: 20 },
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 10 },
periodBtnActive: { backgroundColor: Colors.white, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 },
periodText: { fontSize: 13, fontWeight: '600', color: Colors.gray[500] },
periodTextActive: { color: Colors.black },
loading: { padding: 40, alignItems: 'center' },
loadingText: { color: Colors.gray[400] },
statsRow: { flexDirection: 'row', gap: 10, marginBottom: 16 },
statCard: {
flex: 1, backgroundColor: Colors.gray[50], borderRadius: 16, padding: 16,
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[100],
},
statValue: { fontSize: 22, fontWeight: '800', color: Colors.black },
statLabel: { fontSize: 11, color: Colors.gray[500] },
periodInfo: { marginBottom: 20 },
periodInfoText: { fontSize: 13, color: Colors.gray[400], textAlign: 'center' },
section: { marginBottom: 24 },
sectionTitle: { fontSize: 18, fontWeight: '700', color: Colors.black, marginBottom: 16 },
repartitionItem: { marginBottom: 16 },
repartitionHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 6 },
colorDot: { width: 10, height: 10, borderRadius: 5, marginRight: 8 },
repartitionName: { flex: 1, fontSize: 14, fontWeight: '600', color: Colors.gray[700] },
repartitionPct: { fontSize: 14, fontWeight: '700', color: Colors.black },
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: 3, marginBottom: 4 },
progressFill: { height: 6, borderRadius: 3 },
repartitionFooter: { flexDirection: 'row', justifyContent: 'space-between' },
repartitionDetail: { fontSize: 12, color: Colors.gray[400] },
empty: { alignItems: 'center', paddingTop: 60 },
emptyText: { fontSize: 16, color: Colors.gray[400], marginTop: 12 },
});
+107
View File
@@ -0,0 +1,107 @@
import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { Colors } from '@/lib/colors';
export default function ProfileScreen() {
const router = useRouter();
const { user, logout } = useAuthStore();
const handleLogout = () => {
Alert.alert('Deconnexion', 'Voulez-vous vous deconnecter ?', [
{ text: 'Annuler', style: 'cancel' },
{
text: 'Se deconnecter',
style: 'destructive',
onPress: async () => {
await logout();
router.replace('/auth/login');
},
},
]);
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Avatar & Name */}
<View style={styles.profileHeader}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{user?.prenom?.[0]}{user?.nom?.[0]}
</Text>
</View>
<Text style={styles.name}>{user?.prenom} {user?.nom}</Text>
<Text style={styles.email}>{user?.email}</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleText}>{user?.role?.nom || 'Membre'}</Text>
</View>
</View>
{/* Menu items */}
<View style={styles.menuSection}>
<Text style={styles.sectionTitle}>Compte</Text>
<View style={styles.menuCard}>
<MenuItem icon="person-outline" label="Informations personnelles" />
<MenuItem icon="lock-closed-outline" label="Changer le mot de passe" />
<MenuItem icon="shield-checkmark-outline" label="Donnees personnelles (RGPD)" />
</View>
</View>
<View style={styles.menuSection}>
<Text style={styles.sectionTitle}>Application</Text>
<View style={styles.menuCard}>
<MenuItem icon="information-circle-outline" label="A propos de CESIZen" />
<MenuItem icon="document-text-outline" label="Conditions d'utilisation" />
</View>
</View>
{/* Logout */}
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout}>
<Ionicons name="log-out-outline" size={20} color="#DC143C" />
<Text style={styles.logoutText}>Se deconnecter</Text>
</TouchableOpacity>
<Text style={styles.version}>CESIZen v1.0.0</Text>
</ScrollView>
);
}
function MenuItem({ icon, label }: { icon: string; label: string }) {
return (
<TouchableOpacity style={styles.menuItem}>
<Ionicons name={icon as any} size={20} color={Colors.gray[600]} />
<Text style={styles.menuLabel}>{label}</Text>
<Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} />
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
profileHeader: { alignItems: 'center', marginBottom: 32, marginTop: 12 },
avatar: {
width: 80, height: 80, borderRadius: 40, backgroundColor: Colors.primary,
justifyContent: 'center', alignItems: 'center', marginBottom: 12,
},
avatarText: { fontSize: 28, fontWeight: '800', color: Colors.black },
name: { fontSize: 22, fontWeight: '700', color: Colors.black },
email: { fontSize: 14, color: Colors.gray[500], marginTop: 4 },
roleBadge: { backgroundColor: `${Colors.secondary}20`, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 8, marginTop: 8 },
roleText: { fontSize: 12, fontWeight: '600', color: Colors.secondary, textTransform: 'capitalize' },
menuSection: { marginBottom: 24 },
sectionTitle: { fontSize: 13, fontWeight: '700', color: Colors.gray[400], textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 },
menuCard: { backgroundColor: Colors.gray[50], borderRadius: 16, overflow: 'hidden', borderWidth: 1, borderColor: Colors.gray[100] },
menuItem: {
flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 15, paddingHorizontal: 16,
borderBottomWidth: 1, borderBottomColor: Colors.gray[100],
},
menuLabel: { flex: 1, fontSize: 15, color: Colors.gray[700] },
logoutBtn: {
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8,
paddingVertical: 16, borderRadius: 14, backgroundColor: '#DC143C10',
},
logoutText: { fontSize: 15, fontWeight: '600', color: '#DC143C' },
version: { textAlign: 'center', fontSize: 12, color: Colors.gray[400], marginTop: 20 },
});
+56
View File
@@ -0,0 +1,56 @@
import { useEffect } from 'react';
import { Stack, useRouter, useSegments } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useAuthStore } from '@/lib/auth-store';
import { View, ActivityIndicator } from 'react-native';
const queryClient = new QueryClient();
function AuthGuard({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading, loadToken } = useAuthStore();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
loadToken();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === 'auth';
if (!isAuthenticated && !inAuthGroup) {
router.replace('/auth/login');
} else if (isAuthenticated && inAuthGroup) {
router.replace('/(tabs)');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAuthenticated, isLoading, segments]);
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}>
<ActivityIndicator size="large" color="#fce117" />
</View>
);
}
return <>{children}</>;
}
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<AuthGuard>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="auth" />
<Stack.Screen name="(tabs)" />
</Stack>
</AuthGuard>
<StatusBar style="auto" />
</QueryClientProvider>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { Stack } from 'expo-router';
export default function AuthLayout() {
return <Stack screenOptions={{ headerShown: false }} />;
}
+119
View File
@@ -0,0 +1,119 @@
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 { 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: any) {
Alert.alert('Erreur', error.response?.data?.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>
<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 },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
footerText: { color: Colors.gray[500], fontSize: 14 },
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
});
+111
View File
@@ -0,0 +1,111 @@
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 { Colors } from '@/lib/colors';
export default function RegisterScreen() {
const router = useRouter();
const { register } = useAuthStore();
const [nom, setNom] = useState('');
const [prenom, setPrenom] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirmation, setPasswordConfirmation] = useState('');
const [loading, setLoading] = useState(false);
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;
}
setLoading(true);
try {
await register({ nom, prenom, email, password, password_confirmation: passwordConfirmation });
router.replace('/(tabs)');
} catch (error: any) {
Alert.alert('Erreur', error.response?.data?.message || 'Erreur lors de l\'inscription');
} 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}>Creez votre compte</Text>
</View>
<View style={styles.form}>
<Text style={styles.title}>Inscription</Text>
<View style={styles.row}>
<View style={[styles.inputGroup, { flex: 1, marginRight: 8 }]}>
<Text style={styles.label}>Nom</Text>
<TextInput style={styles.input} value={nom} onChangeText={setNom} placeholder="Dupont" />
</View>
<View style={[styles.inputGroup, { flex: 1, marginLeft: 8 }]}>
<Text style={styles.label}>Prenom</Text>
<TextInput style={styles.input} value={prenom} onChangeText={setPrenom} placeholder="Jean" />
</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" />
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>Mot de passe</Text>
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="Mot de passe" secureTextEntry />
</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 />
</View>
<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>
<Link href="/auth/login" asChild>
<TouchableOpacity><Text style={styles.link}>Se connecter</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: 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 },
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 },
buttonDisabled: { opacity: 0.6 },
buttonText: { fontSize: 16, fontWeight: '700', color: Colors.black },
footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
footerText: { color: Colors.gray[500], fontSize: 14 },
link: { color: Colors.secondary, fontSize: 14, fontWeight: '600' },
});
+7
View File
@@ -0,0 +1,7 @@
import { Redirect } from 'expo-router';
import { useAuthStore } from '@/lib/auth-store';
export default function Index() {
const { isAuthenticated } = useAuthStore();
return <Redirect href={isAuthenticated ? '/(tabs)' : '/auth/login'} />;
}