feat(mobile): refonte design DSFR complète + partie admin

- écrans auth/dashboard/journal/info/urgence/profil refondus sur le design system
- AppBar institutionnel République Française sur tous les écrans
- partie admin (utilisateurs, contenus, émotions) avec AdminGuard
- entrée Espace administration dans le profil (admins uniquement)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Azmog
2026-06-19 15:22:15 +02:00
parent 2e96e07c7e
commit 7fd27d8dc2
22 changed files with 2102 additions and 910 deletions
+2
View File
@@ -47,6 +47,7 @@ export default function TabLayout() {
name="urgence"
options={{
title: 'Urgence',
headerShown: false,
tabBarIcon: ({ color, size }) => <Ionicons name="call" size={size} color={color} />,
}}
/>
@@ -54,6 +55,7 @@ export default function TabLayout() {
name="profile"
options={{
title: 'Profil',
headerShown: false,
tabBarIcon: ({ color, size }) => <Ionicons name="person" size={size} color={color} />,
}}
/>
+126 -92
View File
@@ -1,126 +1,160 @@
import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native';
import { View, Text, StyleSheet, 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';
import RepubliqueHeader from '@/components/RepubliqueHeader';
import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
export default function DashboardScreen() {
const router = useRouter();
const { user } = useAuthStore();
const { data: rapport } = useRapport('month');
const { data: saisies } = useSaisies();
const { data: saisies, isLoading } = useSaisies();
const recentSaisies = (saisies || []).slice(0, 3);
return (
<View style={styles.screen}>
<RepubliqueHeader />
<AppBar
title={`Bonjour ${user?.prenom || ''}`.trim()}
subtitle="Comment allez-vous aujourd'hui ?"
/>
<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>
{/* Actions rapides */}
<View style={styles.actionsRow}>
<Card onPress={() => router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}>
<View style={[styles.actionIcon, styles.actionIconPrimary]}>
<Ionicons name="add-circle" size={28} color={Colors.white} />
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensité moy.</Text>
<Text style={styles.actionTextPrimary}>Nouvelle saisie</Text>
</Card>
<Card onPress={() => router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}>
<View style={[styles.actionIcon, styles.actionIconSecondary]}>
<Ionicons name="bar-chart" size={28} color={Colors.primary} />
</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>
<Text style={styles.actionText}>Mes rapports</Text>
</Card>
</View>
)}
{/* Dernieres saisies */}
{recentSaisies.length > 0 && (
<View style={styles.recentContainer}>
{/* Stats du mois */}
{rapport && (
<View style={styles.block}>
<SectionTitle>Ce mois-ci</SectionTitle>
<Card padding={Spacing.lg}>
<View style={styles.statsRow}>
<View style={styles.statCell}>
<Text style={styles.statValue}>{rapport.stats.total_saisies}</Text>
<Text style={styles.statLabel}>Saisies</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Text style={styles.statValue}>
{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
</Text>
<Text style={styles.statLabel}>Intensité moy.</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Text style={styles.statValue}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel} numberOfLines={1}>
{rapport.stats.emotion_dominante?.nom || 'Aucune'}
</Text>
</View>
</View>
</Card>
</View>
)}
{/* Dernières saisies */}
<View style={styles.block}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Dernières émotions</Text>
<TouchableOpacity onPress={() => router.push('/(tabs)/journal')}>
<Text style={styles.seeAll}>Tout voir</Text>
</TouchableOpacity>
<SectionTitle>Dernières saisies</SectionTitle>
{recentSaisies.length > 0 && (
<Text style={styles.seeAll} onPress={() => router.push('/(tabs)/journal')}>
Tout voir
</Text>
)}
</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')} — Intensité: {saisie.intensite}/10
</Text>
</View>
</View>
))}
{isLoading ? (
<Loader label="Chargement…" />
) : recentSaisies.length === 0 ? (
<EmptyState
icon="sparkles-outline"
title="Aucune saisie pour l'instant"
message="Commencez par enregistrer votre première émotion du jour."
actionLabel="Nouvelle saisie"
onAction={() => router.push('/(tabs)/journal/new')}
/>
) : (
recentSaisies.map((saisie) => (
<Card
key={saisie.id}
onPress={() => router.push('/(tabs)/journal')}
padding={Spacing.md}
style={styles.saisieCard}
>
<View style={[styles.emotionIcon, { backgroundColor: `${saisie.emotion?.couleur}20` }]}>
<Text style={styles.emotionEmoji}>{saisie.emotion?.icone || '🔵'}</Text>
</View>
<View style={styles.saisieContent}>
{saisie.emotion?.couleur ? (
<Badge label={saisie.emotion?.nom || ''} color={saisie.emotion.couleur} />
) : (
<Badge label={saisie.emotion?.nom || '—'} tone="neutral" />
)}
<Text style={styles.saisieDate}>
{new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensité {saisie.intensite}/10
</Text>
</View>
</Card>
))
)}
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: Colors.white },
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.white, borderRadius: 4, padding: 20,
alignItems: 'center', gap: 12, borderWidth: 1, borderColor: Colors.gray[200],
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
actionsRow: { flexDirection: 'row', gap: Spacing.md, marginBottom: Spacing.xl },
actionCard: { flex: 1, alignItems: 'center', gap: Spacing.md },
actionPrimary: {
flex: 1,
alignItems: 'center',
gap: Spacing.md,
backgroundColor: Colors.primary,
borderColor: Colors.primary,
},
actionIcon: { width: 56, height: 56, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
actionText: { fontSize: 14, fontWeight: '600', color: Colors.black },
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: 4, padding: 16,
alignItems: 'center', borderWidth: 1, borderColor: Colors.gray[200],
actionIcon: { width: 56, height: 56, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
actionIconPrimary: { backgroundColor: Colors.primaryDark },
actionIconSecondary: { backgroundColor: '#E3E3FD' },
actionText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.black },
actionTextPrimary: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.white },
block: { marginBottom: Spacing.xl },
statsRow: { flexDirection: 'row', alignItems: 'center' },
statCell: { flex: 1, alignItems: 'center', gap: Spacing.xs },
statDivider: { width: 1, alignSelf: 'stretch', backgroundColor: Colors.gray[200], marginVertical: Spacing.xs },
statValue: { fontSize: FontSize.xxl, fontWeight: FontWeight.heavy, color: Colors.primary },
statLabel: { fontSize: FontSize.tiny, color: Colors.gray[500], textAlign: 'center' },
sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
seeAll: {
fontSize: FontSize.sm,
fontWeight: FontWeight.semibold,
color: Colors.primary,
textDecorationLine: 'underline',
minHeight: MIN_TOUCH,
paddingTop: Spacing.xs,
},
statValue: { fontSize: 24, fontWeight: '800', color: Colors.primary },
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.primary, textDecorationLine: 'underline' },
saisieItem: {
flexDirection: 'row', alignItems: 'center', gap: 12, padding: 14,
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 8,
borderWidth: 1, borderColor: Colors.gray[200],
},
emotionIcon: { width: 44, height: 44, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
saisieCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.sm },
emotionIcon: { width: 44, height: 44, borderRadius: Radius.sm, 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 },
saisieContent: { flex: 1, gap: Spacing.xs, alignItems: 'flex-start' },
saisieDate: { fontSize: FontSize.xs, color: Colors.gray[500] },
});
+36 -24
View File
@@ -1,8 +1,9 @@
import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
import { View, Text, StyleSheet, ScrollView } 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';
import { FontSize, FontWeight, Spacing } from '@/lib/theme';
import { Badge, EmptyState, Loader } from '@/components/ui';
export default function FeedDetailScreen() {
const { slug } = useLocalSearchParams<{ slug: string }>();
@@ -10,17 +11,22 @@ export default function FeedDetailScreen() {
if (isLoading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color={Colors.primary} />
<View style={styles.fill}>
<Stack.Screen options={{ title: 'Article' }} />
<Loader label="Chargement de l'article…" />
</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 style={styles.fill}>
<Stack.Screen options={{ title: 'Article' }} />
<EmptyState
icon="alert-circle-outline"
title="Article introuvable"
message="Cet article n'existe plus ou n'est pas accessible."
/>
</View>
);
}
@@ -28,34 +34,40 @@ export default function FeedDetailScreen() {
const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, '');
return (
<>
<Stack.Screen options={{ title: feed.titre ?? '' }} />
<View style={styles.fill}>
<Stack.Screen options={{ title: feed.titre ?? 'Article' }} />
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.badge}>
<Ionicons name="information-circle" size={14} color={Colors.primary} />
<Text style={styles.badgeText}>Information santé mentale</Text>
</View>
<Badge label="Information santé mentale" tone="primary" />
<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' })}
Publié le{' '}
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
<Text style={styles.body}>{texteNettoye}</Text>
</ScrollView>
</>
</View>
);
}
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.primary}15`, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, marginBottom: 12 },
badgeText: { fontSize: 12, fontWeight: '600', color: Colors.primary },
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.black },
errorText: { marginTop: 12, fontSize: 16, color: Colors.gray[400] },
fill: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1, backgroundColor: Colors.gray[50] },
content: { padding: Spacing.xl, paddingBottom: Spacing.xxxl },
title: {
fontSize: FontSize.xxl,
fontWeight: FontWeight.bold,
color: Colors.black,
marginTop: Spacing.md,
marginBottom: Spacing.sm,
lineHeight: 30,
},
date: { fontSize: FontSize.xs, color: Colors.gray[400], marginBottom: Spacing.xl },
body: { fontSize: FontSize.body, lineHeight: 24, color: Colors.black },
});
+6 -3
View File
@@ -1,17 +1,20 @@
import { Stack } from 'expo-router';
import { Colors } from '@/lib/colors';
import { FontWeight } from '@/lib/theme';
export default function InfoLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: Colors.white },
headerTitleStyle: { fontWeight: '700', color: Colors.black },
headerTitleStyle: { fontWeight: FontWeight.bold, color: Colors.black },
headerTintColor: Colors.primary,
}}
>
<Stack.Screen name="index" options={{ title: 'Infos' }} />
<Stack.Screen name="[slug]" options={{ title: '', headerBackTitle: 'Retour' }} />
{/* La liste rend son propre <AppBar/>, on masque le header natif. */}
<Stack.Screen name="index" options={{ headerShown: false }} />
{/* Le détail garde un header natif avec bouton retour. */}
<Stack.Screen name="[slug]" options={{ title: 'Article', headerBackTitle: 'Retour' }} />
</Stack>
);
}
+63 -60
View File
@@ -1,81 +1,84 @@
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import { View, Text, StyleSheet, FlatList } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import { useFeeds } from '@/hooks/useFeeds';
import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
import { AppBar, Card, EmptyState, Loader } from '@/components/ui';
import type { Feed } from '@/types';
export default function InfoListScreen() {
const { data: feeds, isLoading } = useFeeds();
const router = useRouter();
const renderItem = ({ item }: { item: Feed }) => (
<TouchableOpacity
style={styles.card}
onPress={() => router.push(`/(tabs)/info/${item.slug}`)}
>
<View style={styles.cardIcon}>
<Ionicons name="document-text" size={24} color={Colors.primary} />
</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>
);
const renderItem = ({ item }: { item: Feed }) => {
const extrait = (item.contenu ?? '').replace(/<[^>]*>/g, '').trim();
return (
<Card onPress={() => router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}>
<View style={styles.cardRow}>
<View style={styles.cardIcon}>
<Ionicons name="document-text" size={24} color={Colors.primary} />
</View>
<View style={styles.cardContent}>
<Text style={styles.cardTitle}>{item.titre}</Text>
{extrait ? (
<Text style={styles.cardExcerpt} numberOfLines={2}>
{extrait}
</Text>
) : null}
<Text style={styles.cardDate}>
{new Date(item.created_at).toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={Colors.gray[400]} />
</View>
</Card>
);
};
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}>Santé mentale</Text>
<Text style={styles.headerSubtitle}>Ressources et informations pour votre bien-être</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>
)
}
/>
<AppBar title="Informations" subtitle="Ressources pour votre santé mentale" />
{isLoading ? (
<Loader label="Chargement des articles…" />
) : (
<FlatList
data={feeds ?? []}
keyExtractor={(item) => String(item.id)}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<EmptyState
icon="newspaper-outline"
title="Aucun article disponible"
message="Les ressources d'information apparaîtront ici dès leur publication."
/>
}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
listContent: { padding: 16 },
header: { marginBottom: 20 },
headerTitle: { fontSize: 14, fontWeight: '700', color: Colors.primary, textTransform: 'uppercase', letterSpacing: 1 },
headerSubtitle: { fontSize: 15, color: Colors.gray[500], marginTop: 4 },
card: {
flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16,
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
borderWidth: 1, borderColor: Colors.gray[200],
container: { flex: 1, backgroundColor: Colors.gray[50] },
listContent: { padding: Spacing.lg, gap: Spacing.md },
card: { padding: Spacing.lg },
cardRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
cardIcon: {
width: 48,
height: 48,
borderRadius: Radius.sm,
backgroundColor: '#E3E3FD',
justifyContent: 'center',
alignItems: 'center',
},
cardIcon: { width: 48, height: 48, borderRadius: 4, backgroundColor: `${Colors.primary}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 },
cardTitle: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.xs },
cardExcerpt: { fontSize: FontSize.xs, color: Colors.gray[500], lineHeight: 18 },
cardDate: { fontSize: FontSize.tiny, color: Colors.gray[400], marginTop: Spacing.xs },
});
+5 -3
View File
@@ -1,18 +1,20 @@
import { Stack } from 'expo-router';
import { Colors } from '@/lib/colors';
import { FontWeight } from '@/lib/theme';
export default function JournalLayout() {
return (
<Stack
screenOptions={{
headerStyle: { backgroundColor: Colors.white },
headerTitleStyle: { fontWeight: '700', color: Colors.black },
headerTitleStyle: { fontWeight: FontWeight.bold, color: Colors.black },
headerTintColor: Colors.primary,
}}
>
<Stack.Screen name="index" options={{ title: 'Mon journal' }} />
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="reports" options={{ headerShown: false }} />
<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>
);
}
+34 -82
View File
@@ -1,10 +1,12 @@
import { useState, useEffect } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
import { View, Text, StyleSheet, 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 { IntensityPicker } from './new';
import { Colors } from '@/lib/colors';
import { Spacing, FontSize, FontWeight } from '@/lib/theme';
import { Button, TextField, Loader } from '@/components/ui';
export default function EditEntryScreen() {
const router = useRouter();
@@ -38,126 +40,76 @@ export default function EditEntryScreen() {
note: note.trim() || undefined,
date_saisie: dateSaisie,
});
Alert.alert('Succès', 'Saisie modifiée !', [
{ text: 'OK', onPress: () => router.back() },
]);
Alert.alert('Succès', 'Saisie modifiée !', [{ 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 <Loader label="Chargement…" />;
}
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Modifier la saisie</Text>
{/* Emotion */}
{/* Émotion */}
<View style={styles.section}>
<Text style={styles.label}>Émotion</Text>
<EmotionPicker value={emotionId} onChange={setEmotionId} />
</View>
{/* Intensite */}
{/* Intensité */}
<View style={styles.section}>
<Text style={styles.label}>Intensité</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>
<IntensityPicker value={intensite} onChange={setIntensite} />
<View style={styles.scaleLabels}>
<Text style={styles.scaleLabel}>Faible</Text>
<Text style={styles.scaleLabel}>Forte</Text>
</View>
</View>
{/* Note */}
<View style={styles.section}>
<Text style={styles.label}>Note (optionnel)</Text>
<TextInput
style={styles.textArea}
<TextField
label="Note (optionnel)"
value={note}
onChangeText={setNote}
placeholder="Décrivez ce que vous ressentez..."
placeholder="Décrivez 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]}
<Button title="Annuler" variant="secondary" onPress={() => router.back()} style={styles.flex} />
<Button
title="Enregistrer"
icon="checkmark"
loading={updateSaisie.isPending}
disabled={!emotionId}
onPress={handleSubmit}
disabled={updateSaisie.isPending || !emotionId}
>
<Ionicons name="checkmark" size={20} color={Colors.white} />
<Text style={styles.submitBtnText}>
{updateSaisie.isPending ? 'Envoi...' : 'Enregistrer'}
</Text>
</TouchableOpacity>
style={styles.flex}
/>
</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.black, 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: 4,
backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center',
},
sliderDotActive: { backgroundColor: Colors.primary },
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
sliderDotTextActive: { color: Colors.white },
sliderLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 4 },
sliderLabel: { fontSize: 12, color: Colors.gray[400] },
textArea: {
backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4,
borderBottomWidth: 2, borderBottomColor: Colors.black,
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: 4,
backgroundColor: Colors.white, alignItems: 'center', borderWidth: 1, borderColor: Colors.primary,
},
cancelBtnText: { fontSize: 15, fontWeight: '600', color: Colors.primary },
submitBtn: {
flex: 1, flexDirection: 'row', gap: 8, paddingVertical: 16, borderRadius: 4,
backgroundColor: Colors.primary, alignItems: 'center', justifyContent: 'center',
},
submitBtnText: { fontSize: 15, fontWeight: '700', color: Colors.white },
btnDisabled: { opacity: 0.5 },
container: { flex: 1, backgroundColor: Colors.gray[50] },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
section: { marginBottom: Spacing.xl },
label: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.md },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.lg },
intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: 56 },
intensityUnit: { fontSize: FontSize.xl, color: Colors.gray[400], marginLeft: Spacing.xs },
scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md },
scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] },
actionsRow: { flexDirection: 'row', justifyContent: 'space-between', gap: Spacing.md, marginTop: Spacing.sm },
flex: { flex: 1 },
});
+100 -56
View File
@@ -4,6 +4,8 @@ import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSaisies, useDeleteSaisie } from '@/hooks/useTracker';
import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight, Shadow, MIN_TOUCH, HIT_SLOP } from '@/lib/theme';
import { AppBar, Card, Badge, EmptyState, Loader } from '@/components/ui';
import type { SaisieTracker } from '@/types';
export default function JournalScreen() {
@@ -36,86 +38,128 @@ export default function JournalScreen() {
};
const renderItem = ({ item }: { item: SaisieTracker }) => (
<View style={styles.saisieCard}>
<Card padding={Spacing.lg} 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}>Intensité: {item.intensite}/10</Text>
{item.emotion?.couleur ? (
<Badge label={item.emotion?.nom || ''} color={item.emotion.couleur} />
) : (
<Badge label={item.emotion?.nom || '—'} tone="neutral" />
)}
<Text style={styles.intensityText}>Intensité {item.intensite}/10</Text>
</View>
<View style={styles.intensityBar}>
<View style={[styles.intensityFill, { width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary }]} />
<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>
{item.note ? (
<Text style={styles.note} numberOfLines={2}>
{item.note}
</Text>
) : null}
<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
style={styles.actionBtn}
hitSlop={HIT_SLOP}
accessibilityLabel="Modifier la saisie"
onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}
>
<Ionicons name="create-outline" size={20} color={Colors.primary} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item.id)}>
<Ionicons name="trash-outline" size={18} color={Colors.gray[400]} />
<TouchableOpacity
style={styles.actionBtn}
hitSlop={HIT_SLOP}
accessibilityLabel="Supprimer la saisie"
onPress={() => handleDelete(item.id)}
>
<Ionicons name="trash-outline" size={20} color={Colors.error} />
</TouchableOpacity>
</View>
</View>
</Card>
);
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 première émotion</Text>
</View>
}
/>
<TouchableOpacity style={styles.fab} onPress={() => router.push('/(tabs)/journal/new')}>
<Ionicons name="add" size={28} color={Colors.white} />
<View style={styles.screen}>
<AppBar title="Mon journal" subtitle="Vos émotions au fil du temps" />
{isLoading ? (
<Loader label="Chargement du journal…" />
) : (
<FlatList
data={saisies || []}
keyExtractor={(item) => String(item.id)}
renderItem={renderItem}
contentContainerStyle={styles.listContent}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary} />
}
ListEmptyComponent={
<EmptyState
icon="book-outline"
title="Aucune saisie"
message="Commencez par enregistrer votre première émotion."
actionLabel="Nouvelle saisie"
onAction={() => router.push('/(tabs)/journal/new')}
/>
}
/>
)}
<TouchableOpacity
style={styles.fab}
accessibilityLabel="Nouvelle saisie"
onPress={() => router.push('/(tabs)/journal/new')}
>
<Ionicons name="add" size={30} color={Colors.white} />
</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.white, borderRadius: 4, marginBottom: 10,
borderWidth: 1, borderColor: Colors.gray[200],
},
emotionIcon: { width: 48, height: 48, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
screen: { flex: 1, backgroundColor: Colors.gray[50] },
listContent: { padding: Spacing.lg, paddingBottom: 96 },
saisieCard: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md, marginBottom: Spacing.md },
emotionIcon: { width: 48, height: 48, borderRadius: Radius.sm, 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: 4 },
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 },
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm, flexWrap: 'wrap' },
intensityText: { fontSize: FontSize.xs, color: Colors.gray[500] },
intensityBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm },
intensityFill: { height: 6, borderRadius: Radius.sm },
note: { fontSize: FontSize.xs, color: Colors.gray[600], marginBottom: Spacing.xs, lineHeight: 19 },
date: { fontSize: FontSize.xs, color: Colors.gray[400], textTransform: 'capitalize' },
actions: { gap: Spacing.sm },
actionBtn: {
width: MIN_TOUCH,
height: MIN_TOUCH,
borderRadius: Radius.sm,
justifyContent: 'center',
alignItems: 'center',
},
fab: {
position: 'absolute', right: 20, bottom: 20,
width: 56, height: 56, borderRadius: 4,
backgroundColor: Colors.primary, justifyContent: 'center', alignItems: 'center',
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8,
elevation: 6,
position: 'absolute',
right: Spacing.lg,
bottom: Spacing.lg,
width: 56,
height: 56,
borderRadius: Radius.sm,
backgroundColor: Colors.primary,
justifyContent: 'center',
alignItems: 'center',
...Shadow.raised,
},
});
+89 -95
View File
@@ -1,10 +1,13 @@
import { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, TextInput, ScrollView, Alert } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, 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';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme';
import { Button, TextField } from '@/components/ui';
export default function NewEntryScreen() {
const router = useRouter();
const createSaisie = useCreateSaisie();
@@ -22,11 +25,9 @@ export default function NewEntryScreen() {
note: note.trim() || undefined,
date_saisie: new Date().toISOString().split('T')[0],
});
Alert.alert('Succès', 'Saisie enregistrée !', [
{ text: 'OK', onPress: () => router.back() },
]);
Alert.alert('Succès', 'Saisie enregistrée !', [{ text: 'OK', onPress: () => router.back() }]);
} catch {
Alert.alert('Erreur', 'Impossible d\'enregistrer la saisie');
Alert.alert('Erreur', "Impossible d'enregistrer la saisie");
}
};
@@ -38,7 +39,7 @@ export default function NewEntryScreen() {
<View key={s} style={styles.stepRow}>
<View style={[styles.stepCircle, step >= s && styles.stepActive]}>
{step > s ? (
<Ionicons name="checkmark" size={16} color={Colors.white} />
<Ionicons name="checkmark" size={18} color={Colors.white} />
) : (
<Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text>
)}
@@ -48,23 +49,23 @@ export default function NewEntryScreen() {
))}
</View>
{/* Step 1: Emotion */}
{/* Step 1: Émotion */}
{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.white} />
</TouchableOpacity>
<View style={styles.navRowEnd}>
<Button
title="Suivant"
icon="arrow-forward"
onPress={() => emotionId && setStep(2)}
disabled={!emotionId}
/>
</View>
</View>
)}
{/* Step 2: Intensity */}
{/* Step 2: Intensité */}
{step === 2 && (
<View>
<Text style={styles.stepTitle}>À quel point ressentez-vous cette émotion ?</Text>
@@ -72,37 +73,14 @@ export default function NewEntryScreen() {
<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>
<IntensityPicker value={intensite} onChange={setIntensite} />
<View style={styles.scaleLabels}>
<Text style={styles.scaleLabel}>Faible</Text>
<Text style={styles.scaleLabel}>Forte</Text>
</View>
<View style={styles.navRow}>
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(1)}>
<Ionicons name="arrow-back" size={20} color={Colors.primary} />
<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.white} />
</TouchableOpacity>
<Button title="Retour" variant="secondary" icon="arrow-back" onPress={() => setStep(1)} />
<Button title="Suivant" icon="arrow-forward" onPress={() => setStep(3)} />
</View>
</View>
)}
@@ -111,28 +89,22 @@ export default function NewEntryScreen() {
{step === 3 && (
<View>
<Text style={styles.stepTitle}>Souhaitez-vous ajouter un commentaire ?</Text>
<TextInput
style={styles.textArea}
<TextField
label="Note (optionnel)"
value={note}
onChangeText={setNote}
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion..."
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion…"
multiline
numberOfLines={5}
textAlignVertical="top"
/>
<View style={styles.navRow}>
<TouchableOpacity style={styles.backBtn} onPress={() => setStep(2)}>
<Ionicons name="arrow-back" size={20} color={Colors.primary} />
<Text style={styles.backBtnText}>Retour</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.submitBtn, createSaisie.isPending && styles.btnDisabled]}
<Button title="Retour" variant="secondary" icon="arrow-back" onPress={() => setStep(2)} />
<Button
title="Enregistrer"
icon="checkmark"
loading={createSaisie.isPending}
onPress={handleSubmit}
disabled={createSaisie.isPending}
>
<Ionicons name="checkmark" size={20} color={Colors.white} />
<Text style={styles.nextBtnText}>{createSaisie.isPending ? 'Envoi...' : 'Enregistrer'}</Text>
</TouchableOpacity>
/>
</View>
</View>
)}
@@ -140,46 +112,68 @@ export default function NewEntryScreen() {
);
}
/** Sélecteur d'intensité 1–10, boutons ≥40px, sur deux rangées (wrap). */
export function IntensityPicker({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return (
<View style={pickerStyles.grid}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => {
const active = value === v;
return (
<TouchableOpacity
key={v}
style={[pickerStyles.dot, active && pickerStyles.dotActive]}
onPress={() => onChange(v)}
accessibilityLabel={`Intensité ${v}`}
>
<Text style={[pickerStyles.dotText, active && pickerStyles.dotTextActive]}>{v}</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const pickerStyles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, justifyContent: 'center' },
dot: {
width: 44,
height: 44,
borderRadius: Radius.sm,
backgroundColor: Colors.gray[100],
borderWidth: 1,
borderColor: Colors.gray[200],
justifyContent: 'center',
alignItems: 'center',
},
dotActive: { backgroundColor: Colors.primary, borderColor: Colors.primary },
dotText: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.gray[500] },
dotTextActive: { color: Colors.white },
});
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: 32 },
container: { flex: 1, backgroundColor: Colors.gray[50] },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
stepper: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', marginBottom: Spacing.xxl },
stepRow: { flexDirection: 'row', alignItems: 'center' },
stepCircle: {
width: 32, height: 32, borderRadius: 16, backgroundColor: Colors.gray[200],
justifyContent: 'center', alignItems: 'center',
width: 36,
height: 36,
borderRadius: Radius.pill,
backgroundColor: Colors.gray[200],
justifyContent: 'center',
alignItems: 'center',
},
stepActive: { backgroundColor: Colors.primary },
stepNumber: { fontSize: 14, fontWeight: '700', color: Colors.gray[500] },
stepNumber: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.gray[500] },
stepNumberActive: { color: Colors.white },
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: 4 },
stepLine: { width: 40, height: 3, backgroundColor: Colors.gray[200], marginHorizontal: Spacing.xs },
stepLineActive: { backgroundColor: Colors.primary },
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: 4, backgroundColor: Colors.gray[100], justifyContent: 'center', alignItems: 'center' },
sliderDotActive: { backgroundColor: Colors.primary },
sliderDotText: { fontSize: 12, fontWeight: '600', color: Colors.gray[500] },
sliderDotTextActive: { color: Colors.white },
textArea: {
backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4,
borderBottomWidth: 2, borderBottomColor: Colors.black,
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: 4, backgroundColor: Colors.white, borderWidth: 1, borderColor: Colors.primary },
backBtnText: { fontSize: 15, fontWeight: '600', color: Colors.primary },
nextBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 4, backgroundColor: Colors.primary },
submitBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 14, paddingHorizontal: 24, borderRadius: 4, backgroundColor: Colors.primary },
nextBtnText: { fontSize: 15, fontWeight: '700', color: Colors.white },
btnDisabled: { opacity: 0.5 },
stepTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.xl },
intensityContainer: { flexDirection: 'row', alignItems: 'baseline', justifyContent: 'center', marginBottom: Spacing.xl },
intensityValue: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.primary, lineHeight: 60 },
intensityLabel: { fontSize: FontSize.xxl, color: Colors.gray[400], marginLeft: Spacing.xs },
scaleLabels: { flexDirection: 'row', justifyContent: 'space-between', marginTop: Spacing.md, marginBottom: Spacing.xxl },
scaleLabel: { fontSize: FontSize.xs, color: Colors.gray[500] },
navRow: { flexDirection: 'row', justifyContent: 'space-between', gap: Spacing.md, marginTop: Spacing.md },
navRowEnd: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: Spacing.xl },
});
+130 -119
View File
@@ -3,6 +3,8 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-nati
import { Ionicons } from '@expo/vector-icons';
import { useRapport } from '@/hooks/useRapport';
import { Colors } from '@/lib/colors';
import { Spacing, Radius, FontSize, FontWeight } from '@/lib/theme';
import { AppBar, Card, Badge, SectionTitle, EmptyState, Loader } from '@/components/ui';
import EmotionPieChart from '@/components/EmotionPieChart';
type Period = 'week' | 'month' | 'quarter' | 'year';
@@ -19,130 +21,139 @@ export default function ReportsScreen() {
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 style={styles.screen}>
<AppBar title="Mes rapports" subtitle="Analyse de vos émotions" />
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Sélecteur de période — boutons segmentés */}
<View style={styles.segment}>
{PERIODS.map((p) => {
const active = period === p.value;
return (
<TouchableOpacity
key={p.value}
style={[styles.segmentBtn, active && styles.segmentBtnActive]}
onPress={() => setPeriod(p.value)}
accessibilityLabel={p.label}
>
<Text style={[styles.segmentText, active && styles.segmentTextActive]}>{p.label}</Text>
</TouchableOpacity>
);
})}
</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}>Intensité moy.</Text>
</View>
<View style={styles.statCard}>
<Ionicons name="heart" size={20} color={rapport.stats.emotion_dominante?.couleur || Colors.blueFranceSun} />
<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}>Répartition des émotions</Text>
{/* Camembert (donut) — équivalent mobile du graphique Recharts du web */}
<EmotionPieChart
data={rapport.repartition.map((item) => ({
nom: item.nom,
couleur: item.couleur,
count: item.count,
pourcentage: item.pourcentage,
}))}
/>
<Text style={styles.subSectionTitle}>Détail par émotion</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>
{isLoading ? (
<Loader label="Chargement du rapport…" />
) : rapport && rapport.stats.total_saisies > 0 ? (
<>
{/* Stats */}
<Card padding={Spacing.lg} style={styles.statsCard}>
<View style={styles.statsRow}>
<View style={styles.statCell}>
<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>
)}
</>
) : (
<View style={styles.empty}>
<Ionicons name="bar-chart-outline" size={48} color={Colors.gray[300]} />
<Text style={styles.emptyText}>Pas de données pour cette période</Text>
</View>
)}
</ScrollView>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Ionicons name="trending-up" size={20} color={Colors.primary} />
<Text style={styles.statValue}>{rapport.stats.intensite_moyenne?.toFixed(1) || '—'}</Text>
<Text style={styles.statLabel}>Intensité moy.</Text>
</View>
<View style={styles.statDivider} />
<View style={styles.statCell}>
<Text style={styles.statEmoji}>{rapport.stats.emotion_dominante?.icone || '—'}</Text>
<Text style={styles.statLabel} numberOfLines={1}>
{rapport.stats.emotion_dominante?.nom || 'Aucune'}
</Text>
</View>
</View>
<Text style={styles.periodInfo}>
Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au{' '}
{new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
</Text>
</Card>
{/* Répartition */}
{rapport.repartition.length > 0 && (
<View style={styles.block}>
<SectionTitle>Répartition des émotions</SectionTitle>
<Card padding={Spacing.lg}>
<EmotionPieChart
data={rapport.repartition.map((item) => ({
nom: item.nom,
couleur: item.couleur,
count: item.count,
pourcentage: item.pourcentage,
}))}
/>
</Card>
<View style={styles.detailHeader}>
<SectionTitle>Détail par émotion</SectionTitle>
</View>
<Card padding={Spacing.lg}>
{rapport.repartition.map((item, idx) => (
<View
key={item.nom}
style={[styles.repItem, idx > 0 && styles.repItemBorder]}
>
<View style={styles.repHeader}>
<Badge label={item.nom} color={item.couleur} />
<Text style={styles.repPct}>{item.pourcentage}%</Text>
</View>
<View style={styles.progressBar}>
<View
style={[styles.progressFill, { width: `${item.pourcentage}%`, backgroundColor: item.couleur }]}
/>
</View>
<View style={styles.repFooter}>
<Text style={styles.repDetail}>{item.count} saisies</Text>
<Text style={styles.repDetail}>Moy. {item.intensite_moyenne.toFixed(1)}/10</Text>
</View>
</View>
))}
</Card>
</View>
)}
</>
) : (
<EmptyState
icon="bar-chart-outline"
title="Aucune donnée"
message="Pas de saisie pour cette période. Changez de période ou ajoutez une émotion."
/>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
periodRow: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: 4, padding: 4, marginBottom: 20 },
periodBtn: { flex: 1, paddingVertical: 10, alignItems: 'center', borderRadius: 4 },
periodBtnActive: { backgroundColor: Colors.primary },
periodText: { fontSize: 13, fontWeight: '600', color: Colors.gray[500] },
periodTextActive: { color: Colors.white },
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: 4, padding: 16,
alignItems: 'center', gap: 6, borderWidth: 1, borderColor: Colors.gray[200],
},
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 },
subSectionTitle: { fontSize: 14, fontWeight: '700', color: Colors.gray[600], marginBottom: 12, marginTop: 4 },
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.black },
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 },
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl },
segment: { flexDirection: 'row', backgroundColor: Colors.gray[100], borderRadius: Radius.sm, padding: Spacing.xs, marginBottom: Spacing.lg },
segmentBtn: { flex: 1, paddingVertical: Spacing.sm, alignItems: 'center', borderRadius: Radius.sm, minHeight: 40, justifyContent: 'center' },
segmentBtnActive: { backgroundColor: Colors.primary },
segmentText: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.gray[500] },
segmentTextActive: { color: Colors.white },
statsCard: { marginBottom: Spacing.xl },
statsRow: { flexDirection: 'row', alignItems: 'center' },
statCell: { flex: 1, alignItems: 'center', gap: Spacing.xs },
statDivider: { width: 1, alignSelf: 'stretch', backgroundColor: Colors.gray[200], marginVertical: Spacing.xs },
statValue: { fontSize: FontSize.xxl, fontWeight: FontWeight.heavy, color: Colors.black },
statEmoji: { fontSize: FontSize.xxl },
statLabel: { fontSize: FontSize.tiny, color: Colors.gray[500], textAlign: 'center' },
periodInfo: { fontSize: FontSize.xs, color: Colors.gray[400], textAlign: 'center', marginTop: Spacing.md },
block: { marginBottom: Spacing.xl },
detailHeader: { marginTop: Spacing.lg },
repItem: { paddingVertical: Spacing.md },
repItemBorder: { borderTopWidth: 1, borderTopColor: Colors.gray[200] },
repHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.sm },
repPct: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.black },
progressBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.xs },
progressFill: { height: 6, borderRadius: Radius.sm },
repFooter: { flexDirection: 'row', justifyContent: 'space-between' },
repDetail: { fontSize: FontSize.xs, color: Colors.gray[500] },
});
+109 -59
View File
@@ -3,11 +3,15 @@ import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme';
import { AppBar, Badge, Button, Card, SectionTitle } from '@/components/ui';
export default function ProfileScreen() {
const router = useRouter();
const { user, logout } = useAuthStore();
const isAdmin = user?.role?.nom === 'administrateur';
const handleLogout = () => {
Alert.alert('Déconnexion', 'Voulez-vous vous déconnecter ?', [
{ text: 'Annuler', style: 'cancel' },
@@ -22,86 +26,132 @@ export default function ProfileScreen() {
]);
};
const initiales = `${user?.prenom?.[0] ?? ''}${user?.nom?.[0] ?? ''}`.toUpperCase();
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]}
<View style={styles.screen}>
<AppBar title="Profil" />
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Avatar & identité */}
<Card style={styles.profileCard}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>{initiales}</Text>
</View>
<Text style={styles.name}>
{user?.prenom} {user?.nom}
</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>
<Text style={styles.email}>{user?.email}</Text>
<View style={styles.roleBadge}>
<Badge label={user?.role?.nom ?? 'Membre'} tone="primary" />
</View>
</Card>
{/* 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="Données personnelles (RGPD)" />
{/* Entrée admin — visible uniquement pour les administrateurs */}
{isAdmin ? (
<View style={styles.section}>
<SectionTitle>Administration</SectionTitle>
<Card onPress={() => router.push('/admin')} style={styles.adminCard}>
<View style={styles.adminIcon}>
<Ionicons name="shield-checkmark" size={24} color={Colors.primary} />
</View>
<View style={styles.adminContent}>
<Text style={styles.adminTitle}>Espace administration</Text>
<Text style={styles.adminSubtitle}>Gérer les utilisateurs, contenus et émotions</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={Colors.gray[400]} />
</Card>
</View>
) : null}
{/* Menu compte */}
<View style={styles.section}>
<SectionTitle>Compte</SectionTitle>
<Card padding={0} 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="Données personnelles (RGPD)" last />
</Card>
</View>
</View>
<View style={styles.menuSection}>
<Text style={styles.sectionTitle}>Application</Text>
<View style={styles.menuCard}>
<MenuItem icon="information-circle-outline" label="À propos de CESIZen" />
<MenuItem icon="document-text-outline" label="Conditions d'utilisation" />
{/* Menu application */}
<View style={styles.section}>
<SectionTitle>Application</SectionTitle>
<Card padding={0} style={styles.menuCard}>
<MenuItem icon="information-circle-outline" label="À propos de CESIZen" />
<MenuItem icon="document-text-outline" label="Conditions d'utilisation" last />
</Card>
</View>
</View>
{/* Logout */}
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout}>
<Ionicons name="log-out-outline" size={20} color={Colors.error} />
<Text style={styles.logoutText}>Se déconnecter</Text>
</TouchableOpacity>
{/* Déconnexion */}
<Button title="Se déconnecter" variant="danger" icon="log-out-outline" fullWidth onPress={handleLogout} />
<Text style={styles.version}>CESIZen v1.0.0</Text>
</ScrollView>
<Text style={styles.version}>CESIZen v1.0.0</Text>
</ScrollView>
</View>
);
}
function MenuItem({ icon, label }: { icon: keyof typeof Ionicons.glyphMap; label: string }) {
function MenuItem({
icon,
label,
last = false,
}: {
icon: keyof typeof Ionicons.glyphMap;
label: string;
last?: boolean;
}) {
return (
<TouchableOpacity style={styles.menuItem}>
<TouchableOpacity style={[styles.menuItem, last && styles.menuItemLast]} accessibilityRole="button">
<Ionicons name={icon} size={20} color={Colors.gray[600]} />
<Text style={styles.menuLabel}>{label}</Text>
<Ionicons name="chevron-forward" size={18} color={Colors.gray[300]} />
<Ionicons name="chevron-forward" size={18} color={Colors.gray[400]} />
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.white },
content: { padding: 20, paddingBottom: 40 },
profileHeader: { alignItems: 'center', marginBottom: 32, marginTop: 12 },
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxxl, gap: Spacing.xl },
profileCard: { alignItems: 'center', paddingVertical: Spacing.xl },
avatar: {
width: 80, height: 80, borderRadius: 4, backgroundColor: Colors.primary,
justifyContent: 'center', alignItems: 'center', marginBottom: 12,
width: 80,
height: 80,
borderRadius: Radius.sm,
backgroundColor: Colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
avatarText: { fontSize: 28, fontWeight: '800', color: Colors.white },
name: { fontSize: 22, fontWeight: '700', color: Colors.black },
email: { fontSize: 14, color: Colors.gray[500], marginTop: 4 },
roleBadge: { backgroundColor: `${Colors.primary}15`, paddingHorizontal: 12, paddingVertical: 4, borderRadius: 4, marginTop: 8 },
roleText: { fontSize: 12, fontWeight: '600', color: Colors.primary, textTransform: 'capitalize' },
menuSection: { marginBottom: 24 },
sectionTitle: { fontSize: 13, fontWeight: '700', color: Colors.gray[500], textTransform: 'uppercase', letterSpacing: 1, marginBottom: 8 },
menuCard: { backgroundColor: Colors.white, borderRadius: 4, overflow: 'hidden', borderWidth: 1, borderColor: Colors.gray[200] },
avatarText: { fontSize: FontSize.display, fontWeight: FontWeight.heavy, color: Colors.white },
name: { fontSize: FontSize.xl, fontWeight: FontWeight.bold, color: Colors.black },
email: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: Spacing.xs },
roleBadge: { marginTop: Spacing.sm },
section: { gap: Spacing.sm },
adminCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, padding: Spacing.lg },
adminIcon: {
width: 48,
height: 48,
borderRadius: Radius.sm,
backgroundColor: '#E3E3FD',
justifyContent: 'center',
alignItems: 'center',
},
adminContent: { flex: 1 },
adminTitle: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: 2 },
adminSubtitle: { fontSize: FontSize.xs, color: Colors.gray[500], lineHeight: 18 },
menuCard: { overflow: 'hidden' },
menuItem: {
flexDirection: 'row', alignItems: 'center', gap: 12, paddingVertical: 15, paddingHorizontal: 16,
borderBottomWidth: 1, borderBottomColor: Colors.gray[200],
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
minHeight: MIN_TOUCH,
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.lg,
borderBottomWidth: 1,
borderBottomColor: Colors.gray[200],
},
menuLabel: { flex: 1, fontSize: 15, color: Colors.black },
logoutBtn: {
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8,
paddingVertical: 16, borderRadius: 4, backgroundColor: Colors.errorBg,
},
logoutText: { fontSize: 15, fontWeight: '600', color: Colors.error },
version: { textAlign: 'center', fontSize: 12, color: Colors.gray[400], marginTop: 20 },
menuItemLast: { borderBottomWidth: 0 },
menuLabel: { flex: 1, fontSize: FontSize.body, color: Colors.black },
version: { textAlign: 'center', fontSize: FontSize.xs, color: Colors.gray[400] },
});
+67 -48
View File
@@ -1,6 +1,8 @@
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Linking, Alert } from 'react-native';
import { View, Text, StyleSheet, ScrollView, Linking, Alert } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors';
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
import { AppBar, Button, Card } from '@/components/ui';
type Contact = {
nom: string;
@@ -65,61 +67,78 @@ export default function UrgenceScreen() {
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<View style={styles.header}>
<View style={styles.iconBadge}>
<Ionicons name="heart" size={20} color={Colors.error} />
</View>
<Text style={styles.headerTitle}>Vous n'êtes pas seul(e)</Text>
<Text style={styles.headerSubtitle}>
<View style={styles.screen}>
<AppBar title="Urgence" subtitle="Vous n'êtes pas seul(e)" />
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.intro}>
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={Colors.error} />
</View>
<View style={styles.cardContent}>
<Text style={styles.cardName}>{c.nom}</Text>
<Text style={styles.cardNumber}>{formatNumero(c.numero)}</Text>
{CONTACTS.map((c) => (
<Card key={c.numero} style={styles.card}>
<View style={styles.cardHeader}>
<View style={styles.cardIcon}>
<Ionicons name="call" size={22} color={Colors.error} />
</View>
<View style={styles.cardHeaderText}>
<Text style={styles.cardName}>{c.nom}</Text>
<Text style={styles.cardNumber}>{formatNumero(c.numero)}</Text>
</View>
</View>
<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.cardAvailabilityRow}>
<Ionicons name="time-outline" size={14} color={Colors.gray[500]} />
<Text style={styles.cardAvailability}>{c.disponibilite}</Text>
</View>
<Button
title={`Appeler le ${formatNumero(c.numero)}`}
variant="danger"
icon="call"
fullWidth
onPress={() => appeler(c.numero, c.nom)}
style={styles.cardButton}
/>
</Card>
))}
<View style={styles.warning}>
<Ionicons name="warning" size={18} color={Colors.error} />
<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>
<Card style={styles.warning} padding={Spacing.lg}>
<View style={styles.warningRow}>
<Ionicons name="warning" size={20} color={Colors.error} />
<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>
</Card>
</ScrollView>
</View>
);
}
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: 4, backgroundColor: Colors.errorBg, 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.white, borderRadius: 4, marginBottom: 10,
borderWidth: 1, borderColor: Colors.gray[200],
screen: { flex: 1, backgroundColor: Colors.gray[50] },
container: { flex: 1 },
content: { padding: Spacing.lg, paddingBottom: Spacing.xxl, gap: Spacing.md },
intro: { fontSize: FontSize.sm, color: Colors.gray[500], lineHeight: 20, marginBottom: Spacing.xs },
card: { padding: Spacing.lg, gap: Spacing.sm },
cardHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
cardIcon: {
width: 44,
height: 44,
borderRadius: Radius.sm,
backgroundColor: Colors.errorBg,
justifyContent: 'center',
alignItems: 'center',
},
cardIcon: { width: 44, height: 44, borderRadius: 4, backgroundColor: Colors.errorBg, justifyContent: 'center', alignItems: 'center' },
cardContent: { flex: 1 },
cardName: { fontSize: 14, fontWeight: '700', color: Colors.black, marginBottom: 2 },
cardNumber: { fontSize: 20, fontWeight: '800', color: Colors.error, marginBottom: 6 },
cardDescription: { fontSize: 12, color: Colors.gray[500], lineHeight: 17 },
cardAvailability: { fontSize: 11, color: Colors.primary, marginTop: 4, fontWeight: '600' },
warning: { marginTop: 8, padding: 14, borderRadius: 4, backgroundColor: Colors.errorBg, borderWidth: 1, borderColor: Colors.error, flexDirection: 'row', gap: 10, alignItems: 'flex-start' },
warningText: { flex: 1, fontSize: 13, color: Colors.error, lineHeight: 19 },
bold: { fontWeight: '800' },
cardHeaderText: { flex: 1 },
cardName: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: 2 },
cardNumber: { fontSize: FontSize.xl, fontWeight: FontWeight.heavy, color: Colors.error },
cardDescription: { fontSize: FontSize.xs, color: Colors.gray[600], lineHeight: 19 },
cardAvailabilityRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs },
cardAvailability: { fontSize: FontSize.tiny, color: Colors.gray[500], fontWeight: FontWeight.semibold },
cardButton: { marginTop: Spacing.xs },
warning: { backgroundColor: Colors.errorBg, borderColor: Colors.error },
warningRow: { flexDirection: 'row', gap: Spacing.md, alignItems: 'flex-start' },
warningText: { flex: 1, fontSize: FontSize.xs, color: Colors.error, lineHeight: 19 },
bold: { fontWeight: FontWeight.heavy },
});