diff --git a/mobile/app/(tabs)/_layout.tsx b/mobile/app/(tabs)/_layout.tsx
index fab5ee7..3f1c880 100644
--- a/mobile/app/(tabs)/_layout.tsx
+++ b/mobile/app/(tabs)/_layout.tsx
@@ -47,6 +47,7 @@ export default function TabLayout() {
name="urgence"
options={{
title: 'Urgence',
+ headerShown: false,
tabBarIcon: ({ color, size }) => ,
}}
/>
@@ -54,6 +55,7 @@ export default function TabLayout() {
name="profile"
options={{
title: 'Profil',
+ headerShown: false,
tabBarIcon: ({ color, size }) => ,
}}
/>
diff --git a/mobile/app/(tabs)/index.tsx b/mobile/app/(tabs)/index.tsx
index 17b11f4..ce71133 100644
--- a/mobile/app/(tabs)/index.tsx
+++ b/mobile/app/(tabs)/index.tsx
@@ -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 (
-
+
- Bonjour, {user?.prenom} 👋
- Comment allez-vous aujourd'hui ?
-
- {/* Actions rapides */}
-
- router.push('/(tabs)/journal/new')}>
-
-
-
- Nouvelle saisie
-
- router.push('/(tabs)/journal/reports')}>
-
-
-
- Mes rapports
-
-
-
- {/* Stats du mois */}
- {rapport && (
-
- Ce mois-ci
-
-
- {rapport.stats.total_saisies}
- Saisies
+ {/* Actions rapides */}
+
+ router.push('/(tabs)/journal/new')} padding={Spacing.lg} style={styles.actionPrimary}>
+
+
-
- {rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
- Intensité moy.
+ Nouvelle saisie
+
+ router.push('/(tabs)/journal/reports')} padding={Spacing.lg} style={styles.actionCard}>
+
+
-
- {rapport.stats.emotion_dominante?.icone || '—'}
- {rapport.stats.emotion_dominante?.nom || 'Aucune'}
-
-
+ Mes rapports
+
- )}
- {/* Dernieres saisies */}
- {recentSaisies.length > 0 && (
-
+ {/* Stats du mois */}
+ {rapport && (
+
+ Ce mois-ci
+
+
+
+ {rapport.stats.total_saisies}
+ Saisies
+
+
+
+
+ {rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
+
+ Intensité moy.
+
+
+
+ {rapport.stats.emotion_dominante?.icone || '—'}
+
+ {rapport.stats.emotion_dominante?.nom || 'Aucune'}
+
+
+
+
+
+ )}
+
+ {/* Dernières saisies */}
+
- Dernières émotions
- router.push('/(tabs)/journal')}>
- Tout voir
-
+ Dernières saisies
+ {recentSaisies.length > 0 && (
+ router.push('/(tabs)/journal')}>
+ Tout voir
+
+ )}
- {recentSaisies.map((saisie) => (
-
-
- {saisie.emotion?.icone || '🔵'}
-
-
- {saisie.emotion?.nom}
-
- {new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensité: {saisie.intensite}/10
-
-
-
- ))}
+
+ {isLoading ? (
+
+ ) : recentSaisies.length === 0 ? (
+ router.push('/(tabs)/journal/new')}
+ />
+ ) : (
+ recentSaisies.map((saisie) => (
+ router.push('/(tabs)/journal')}
+ padding={Spacing.md}
+ style={styles.saisieCard}
+ >
+
+ {saisie.emotion?.icone || '🔵'}
+
+
+ {saisie.emotion?.couleur ? (
+
+ ) : (
+
+ )}
+
+ {new Date(saisie.date_saisie).toLocaleDateString('fr-FR')} — Intensité {saisie.intensite}/10
+
+
+
+ ))
+ )}
- )}
);
}
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] },
});
diff --git a/mobile/app/(tabs)/info/[slug].tsx b/mobile/app/(tabs)/info/[slug].tsx
index 0700195..ac1ba77 100644
--- a/mobile/app/(tabs)/info/[slug].tsx
+++ b/mobile/app/(tabs)/info/[slug].tsx
@@ -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 (
-
-
+
+
+
);
}
if (isError || !feed) {
return (
-
-
- Article introuvable
+
+
+
);
}
@@ -28,34 +34,40 @@ export default function FeedDetailScreen() {
const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, '');
return (
- <>
-
+
+
-
-
- Information santé mentale
-
+
{feed.titre}
- 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',
+ })}
{texteNettoye}
- >
+
);
}
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 },
});
diff --git a/mobile/app/(tabs)/info/_layout.tsx b/mobile/app/(tabs)/info/_layout.tsx
index 16af280..a259e6d 100644
--- a/mobile/app/(tabs)/info/_layout.tsx
+++ b/mobile/app/(tabs)/info/_layout.tsx
@@ -1,17 +1,20 @@
import { Stack } from 'expo-router';
import { Colors } from '@/lib/colors';
+import { FontWeight } from '@/lib/theme';
export default function InfoLayout() {
return (
-
-
+ {/* La liste rend son propre , on masque le header natif. */}
+
+ {/* Le détail garde un header natif avec bouton retour. */}
+
);
}
diff --git a/mobile/app/(tabs)/info/index.tsx b/mobile/app/(tabs)/info/index.tsx
index 702e6a7..b41a7d3 100644
--- a/mobile/app/(tabs)/info/index.tsx
+++ b/mobile/app/(tabs)/info/index.tsx
@@ -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 }) => (
- router.push(`/(tabs)/info/${item.slug}`)}
- >
-
-
-
-
- {item.titre}
-
- {item.contenu?.replace(/<[^>]*>/g, '').substring(0, 120)}...
-
-
- {new Date(item.created_at).toLocaleDateString('fr-FR')}
-
-
-
-
- );
+ const renderItem = ({ item }: { item: Feed }) => {
+ const extrait = (item.contenu ?? '').replace(/<[^>]*>/g, '').trim();
+ return (
+ router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}>
+
+
+
+
+
+ {item.titre}
+ {extrait ? (
+
+ {extrait}
+
+ ) : null}
+
+ {new Date(item.created_at).toLocaleDateString('fr-FR', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ })}
+
+
+
+
+
+ );
+ };
return (
- String(item.id)}
- renderItem={renderItem}
- contentContainerStyle={styles.listContent}
- ListHeaderComponent={
-
- Santé mentale
- Ressources et informations pour votre bien-être
-
- }
- ListEmptyComponent={
- isLoading ? (
- Chargement...
- ) : (
-
-
- Aucun article disponible
-
- )
- }
- />
+
+ {isLoading ? (
+
+ ) : (
+ String(item.id)}
+ renderItem={renderItem}
+ contentContainerStyle={styles.listContent}
+ ListEmptyComponent={
+
+ }
+ />
+ )}
);
}
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 },
});
diff --git a/mobile/app/(tabs)/journal/_layout.tsx b/mobile/app/(tabs)/journal/_layout.tsx
index 426faf7..3741465 100644
--- a/mobile/app/(tabs)/journal/_layout.tsx
+++ b/mobile/app/(tabs)/journal/_layout.tsx
@@ -1,18 +1,20 @@
import { Stack } from 'expo-router';
import { Colors } from '@/lib/colors';
+import { FontWeight } from '@/lib/theme';
export default function JournalLayout() {
return (
-
+
+
-
);
}
diff --git a/mobile/app/(tabs)/journal/edit.tsx b/mobile/app/(tabs)/journal/edit.tsx
index 22a32e3..a844729 100644
--- a/mobile/app/(tabs)/journal/edit.tsx
+++ b/mobile/app/(tabs)/journal/edit.tsx
@@ -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 (
-
- Chargement...
-
- );
+ return ;
}
return (
- Modifier la saisie
-
- {/* Emotion */}
+ {/* Émotion */}
Émotion
- {/* Intensite */}
+ {/* Intensité */}
Intensité
{intensite}
/10
-
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
- setIntensite(v)}
- >
- {v}
-
- ))}
-
-
- Faible
- Forte
+
+
+ Faible
+ Forte
{/* Note */}
- Note (optionnel)
-
{/* Actions */}
- router.back()}>
- Annuler
-
- router.back()} style={styles.flex} />
+
+ style={styles.flex}
+ />
);
}
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 },
});
diff --git a/mobile/app/(tabs)/journal/index.tsx b/mobile/app/(tabs)/journal/index.tsx
index 37d6599..e775c12 100644
--- a/mobile/app/(tabs)/journal/index.tsx
+++ b/mobile/app/(tabs)/journal/index.tsx
@@ -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 }) => (
-
+
{item.emotion?.icone || '🔵'}
-
- {item.emotion?.nom}
-
- Intensité: {item.intensite}/10
+ {item.emotion?.couleur ? (
+
+ ) : (
+
+ )}
+ Intensité {item.intensite}/10
-
+
- {item.note && {item.note}}
- {new Date(item.date_saisie).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
+ {item.note ? (
+
+ {item.note}
+
+ ) : null}
+
+ {new Date(item.date_saisie).toLocaleDateString('fr-FR', {
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ })}
+
- router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}>
-
+ router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}
+ >
+
- handleDelete(item.id)}>
-
+ handleDelete(item.id)}
+ >
+
-
+
);
return (
-
- String(item.id)}
- renderItem={renderItem}
- contentContainerStyle={styles.listContent}
- refreshControl={}
- ListEmptyComponent={
-
-
- Aucune saisie
- Commencez par enregistrer votre première émotion
-
- }
- />
- router.push('/(tabs)/journal/new')}>
-
+
+
+ {isLoading ? (
+
+ ) : (
+ String(item.id)}
+ renderItem={renderItem}
+ contentContainerStyle={styles.listContent}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+ router.push('/(tabs)/journal/new')}
+ />
+ }
+ />
+ )}
+ router.push('/(tabs)/journal/new')}
+ >
+
);
}
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,
},
});
diff --git a/mobile/app/(tabs)/journal/new.tsx b/mobile/app/(tabs)/journal/new.tsx
index 6e0a62f..1db654d 100644
--- a/mobile/app/(tabs)/journal/new.tsx
+++ b/mobile/app/(tabs)/journal/new.tsx
@@ -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() {
= s && styles.stepActive]}>
{step > s ? (
-
+
) : (
= s && styles.stepNumberActive]}>{s}
)}
@@ -48,23 +49,23 @@ export default function NewEntryScreen() {
))}
- {/* Step 1: Emotion */}
+ {/* Step 1: Émotion */}
{step === 1 && (
Comment vous sentez-vous ?
- emotionId && setStep(2)}
- disabled={!emotionId}
- >
- Suivant
-
-
+
+
)}
- {/* Step 2: Intensity */}
+ {/* Step 2: Intensité */}
{step === 2 && (
À quel point ressentez-vous cette émotion ?
@@ -72,37 +73,14 @@ export default function NewEntryScreen() {
{intensite}
/10
-
- Faible
-
-
-
-
-
-
- {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => (
- setIntensite(v)}
- >
- {v}
-
- ))}
-
-
-
- Forte
+
+
+ Faible
+ Forte
- setStep(1)}>
-
- Retour
-
- setStep(3)}>
- Suivant
-
-
+
)}
@@ -111,28 +89,22 @@ export default function NewEntryScreen() {
{step === 3 && (
Souhaitez-vous ajouter un commentaire ?
-
- setStep(2)}>
-
- Retour
-
- setStep(2)} />
+
-
- {createSaisie.isPending ? 'Envoi...' : 'Enregistrer'}
-
+ />
)}
@@ -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 (
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => {
+ const active = value === v;
+ return (
+ onChange(v)}
+ accessibilityLabel={`Intensité ${v}`}
+ >
+ {v}
+
+ );
+ })}
+
+ );
+}
+
+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 },
});
diff --git a/mobile/app/(tabs)/journal/reports.tsx b/mobile/app/(tabs)/journal/reports.tsx
index 05b5120..0f52fa8 100644
--- a/mobile/app/(tabs)/journal/reports.tsx
+++ b/mobile/app/(tabs)/journal/reports.tsx
@@ -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 (
-
- {/* Period selector */}
-
- {PERIODS.map((p) => (
- setPeriod(p.value)}
- >
-
- {p.label}
-
-
- ))}
-
-
- {isLoading ? (
-
- Chargement...
+
+
+
+ {/* Sélecteur de période — boutons segmentés */}
+
+ {PERIODS.map((p) => {
+ const active = period === p.value;
+ return (
+ setPeriod(p.value)}
+ accessibilityLabel={p.label}
+ >
+ {p.label}
+
+ );
+ })}
- ) : rapport ? (
- <>
- {/* Stats cards */}
-
-
-
- {rapport.stats.total_saisies}
- Saisies
-
-
-
- {rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
- Intensité moy.
-
-
-
- {rapport.stats.emotion_dominante?.icone || '—'}
- {rapport.stats.emotion_dominante?.nom || 'Aucune'}
-
-
- {/* Period info */}
-
-
- Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au {new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
-
-
-
- {/* Repartition */}
- {rapport.repartition.length > 0 && (
-
- Répartition des émotions
-
- {/* Camembert (donut) — équivalent mobile du graphique Recharts du web */}
- ({
- nom: item.nom,
- couleur: item.couleur,
- count: item.count,
- pourcentage: item.pourcentage,
- }))}
- />
-
- Détail par émotion
- {rapport.repartition.map((item) => (
-
-
-
- {item.nom}
- {item.pourcentage}%
-
-
-
-
-
- {item.count} saisies
- Moy: {item.intensite_moyenne.toFixed(1)}/10
-
+ {isLoading ? (
+
+ ) : rapport && rapport.stats.total_saisies > 0 ? (
+ <>
+ {/* Stats */}
+
+
+
+
+ {rapport.stats.total_saisies}
+ Saisies
- ))}
-
- )}
- >
- ) : (
-
-
- Pas de données pour cette période
-
- )}
-
+
+
+
+ {rapport.stats.intensite_moyenne?.toFixed(1) || '—'}
+ Intensité moy.
+
+
+
+ {rapport.stats.emotion_dominante?.icone || '—'}
+
+ {rapport.stats.emotion_dominante?.nom || 'Aucune'}
+
+
+
+
+ Du {new Date(rapport.date_debut).toLocaleDateString('fr-FR')} au{' '}
+ {new Date(rapport.date_fin).toLocaleDateString('fr-FR')}
+
+
+
+ {/* Répartition */}
+ {rapport.repartition.length > 0 && (
+
+ Répartition des émotions
+
+
+ ({
+ nom: item.nom,
+ couleur: item.couleur,
+ count: item.count,
+ pourcentage: item.pourcentage,
+ }))}
+ />
+
+
+
+ Détail par émotion
+
+
+ {rapport.repartition.map((item, idx) => (
+ 0 && styles.repItemBorder]}
+ >
+
+
+ {item.pourcentage}%
+
+
+
+
+
+ {item.count} saisies
+ Moy. {item.intensite_moyenne.toFixed(1)}/10
+
+
+ ))}
+
+
+ )}
+ >
+ ) : (
+
+ )}
+
+
);
}
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] },
});
diff --git a/mobile/app/(tabs)/profile.tsx b/mobile/app/(tabs)/profile.tsx
index a238bad..09b9337 100644
--- a/mobile/app/(tabs)/profile.tsx
+++ b/mobile/app/(tabs)/profile.tsx
@@ -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 (
-
- {/* Avatar & Name */}
-
-
-
- {user?.prenom?.[0]}{user?.nom?.[0]}
+
+
+
+ {/* Avatar & identité */}
+
+
+ {initiales}
+
+
+ {user?.prenom} {user?.nom}
-
- {user?.prenom} {user?.nom}
- {user?.email}
-
- {user?.role?.nom || 'Membre'}
-
-
+ {user?.email}
+
+
+
+
- {/* Menu items */}
-
- Compte
-
-
-
-
+ {/* Entrée admin — visible uniquement pour les administrateurs */}
+ {isAdmin ? (
+
+ Administration
+ router.push('/admin')} style={styles.adminCard}>
+
+
+
+
+ Espace administration
+ Gérer les utilisateurs, contenus et émotions
+
+
+
+
+ ) : null}
+
+ {/* Menu compte */}
+
+ Compte
+
+
+
+
+
-
-
- Application
-
-
-
+ {/* Menu application */}
+
+ Application
+
+
+
+
-
- {/* Logout */}
-
-
- Se déconnecter
-
+ {/* Déconnexion */}
+
- CESIZen v1.0.0
-
+ CESIZen v1.0.0
+
+
);
}
-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 (
-
+
{label}
-
+
);
}
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] },
});
diff --git a/mobile/app/(tabs)/urgence.tsx b/mobile/app/(tabs)/urgence.tsx
index 4db180b..e1a0b66 100644
--- a/mobile/app/(tabs)/urgence.tsx
+++ b/mobile/app/(tabs)/urgence.tsx
@@ -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 (
-
-
-
-
-
- Vous n'êtes pas seul(e)
-
+
+
+
+
Si vous ou un proche traversez une période difficile, ces services sont là pour vous écouter.
-
- {CONTACTS.map((c) => (
- appeler(c.numero, c.nom)}>
-
-
-
-
- {c.nom}
- {formatNumero(c.numero)}
+ {CONTACTS.map((c) => (
+
+
+
+
+
+
+ {c.nom}
+ {formatNumero(c.numero)}
+
+
{c.description}
- {c.disponibilite}
-
-
-
- ))}
+
+
+ {c.disponibilite}
+
+ appeler(c.numero, c.nom)}
+ style={styles.cardButton}
+ />
+
+ ))}
-
-
-
- En cas d'urgence vitale, appelez immédiatement le 15 (SAMU) ou le 112.
-
-
-
+
+
+
+
+ En cas d'urgence vitale, appelez immédiatement le 15 (SAMU) ou le{' '}
+ 112.
+
+
+
+
+
);
}
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 },
});
diff --git a/mobile/app/admin/_layout.tsx b/mobile/app/admin/_layout.tsx
new file mode 100644
index 0000000..4a1ba8f
--- /dev/null
+++ b/mobile/app/admin/_layout.tsx
@@ -0,0 +1,23 @@
+import { Stack } from 'expo-router';
+import AdminGuard from '@/components/AdminGuard';
+import { Colors } from '@/lib/colors';
+
+/** Pile de navigation de l'espace d'administration (protégée par AdminGuard). */
+export default function AdminLayout() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/mobile/app/admin/contenus.tsx b/mobile/app/admin/contenus.tsx
new file mode 100644
index 0000000..e51522e
--- /dev/null
+++ b/mobile/app/admin/contenus.tsx
@@ -0,0 +1,217 @@
+import { useState } from 'react';
+import {
+ Alert,
+ FlatList,
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ View,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { Badge, Button, Card, EmptyState, Loader, TextField } from '@/components/ui';
+import { useAdminFeeds, useSaveFeed, useDeleteFeed } from '@/hooks/useAdmin';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme';
+import type { Feed } from '@/types';
+
+interface FormState {
+ id?: number;
+ titre: string;
+ contenu: string;
+ ordre: string;
+ est_publie: boolean;
+}
+
+const FORM_VIDE: FormState = { titre: '', contenu: '', ordre: '0', est_publie: false };
+
+function messageErreur(e: unknown, defaut: string): string {
+ const err = e as { response?: { data?: { message?: string } } };
+ return err?.response?.data?.message ?? defaut;
+}
+
+export default function AdminContenusScreen() {
+ const { data: feeds, isLoading } = useAdminFeeds();
+ const saveMut = useSaveFeed();
+ const deleteMut = useDeleteFeed();
+
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form, setForm] = useState(FORM_VIDE);
+
+ const ouvrirCreation = () => {
+ setForm(FORM_VIDE);
+ setModalVisible(true);
+ };
+
+ const ouvrirEdition = (f: Feed) => {
+ setForm({ id: f.id, titre: f.titre, contenu: f.contenu, ordre: String(f.ordre), est_publie: f.est_publie });
+ setModalVisible(true);
+ };
+
+ const set = (cle: 'titre' | 'contenu' | 'ordre') => (valeur: string) => setForm((f) => ({ ...f, [cle]: valeur }));
+
+ const soumettre = async () => {
+ if (!form.titre.trim() || !form.contenu.trim()) {
+ Alert.alert('Champs requis', 'Le titre et le contenu sont obligatoires.');
+ return;
+ }
+ try {
+ await saveMut.mutateAsync({
+ id: form.id,
+ titre: form.titre.trim(),
+ contenu: form.contenu.trim(),
+ ordre: Number.parseInt(form.ordre, 10) || 0,
+ est_publie: form.est_publie,
+ });
+ setModalVisible(false);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Enregistrement impossible.'));
+ }
+ };
+
+ const supprimer = (f: Feed) => {
+ Alert.alert('Supprimer', `Supprimer « ${f.titre} » ?`, [
+ { text: 'Annuler', style: 'cancel' },
+ {
+ text: 'Supprimer',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await deleteMut.mutateAsync(f.id);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Suppression impossible.'));
+ }
+ },
+ },
+ ]);
+ };
+
+ const renderItem = ({ item }: { item: Feed }) => (
+
+ {item.titre}
+
+
+
+
+
+ {item.contenu}
+
+
+ ouvrirEdition(item)} />
+ supprimer(item)} />
+
+
+ );
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ String(item.id)}
+ renderItem={renderItem}
+ contentContainerStyle={styles.list}
+ ListHeaderComponent={
+
+
+
+ }
+ ListEmptyComponent={
+
+ }
+ />
+
+ setModalVisible(false)}>
+
+
+
+ {form.id ? 'Modifier le contenu' : 'Nouveau contenu'}
+ setModalVisible(false)} accessibilityLabel="Fermer">
+
+
+
+
+
+
+
+
+
+
+ Publié
+
+ {form.est_publie ? 'Visible par le public' : 'Brouillon (masqué)'}
+
+
+ setForm((f) => ({ ...f, est_publie: v }))}
+ trackColor={{ true: Colors.primary, false: Colors.gray[300] }}
+ thumbColor={Colors.white}
+ />
+
+
+
+ setModalVisible(false)} />
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: { flex: 1, backgroundColor: Colors.gray[50] },
+ list: { padding: Spacing.lg, gap: Spacing.md },
+ headerAction: { marginBottom: Spacing.md },
+ card: { padding: Spacing.lg, gap: Spacing.sm },
+ titre: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.black },
+ badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' },
+ extrait: { fontSize: FontSize.sm, color: Colors.gray[500], lineHeight: 20 },
+ actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs },
+ modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' },
+ modalSheet: {
+ backgroundColor: Colors.white,
+ borderTopLeftRadius: Radius.md,
+ borderTopRightRadius: Radius.md,
+ maxHeight: '90%',
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: Spacing.lg,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray[200],
+ },
+ modalTitle: { fontSize: FontSize.lg, fontWeight: FontWeight.bold, color: Colors.black },
+ modalBody: { padding: Spacing.lg },
+ fieldLabel: { fontSize: FontSize.sm, fontWeight: FontWeight.medium, color: Colors.black },
+ switchRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: Spacing.sm,
+ marginBottom: Spacing.md,
+ },
+ switchText: { flex: 1, paddingRight: Spacing.md },
+ switchHelper: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 },
+ modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: Spacing.sm, marginTop: Spacing.sm },
+});
diff --git a/mobile/app/admin/emotions.tsx b/mobile/app/admin/emotions.tsx
new file mode 100644
index 0000000..0bf1cc9
--- /dev/null
+++ b/mobile/app/admin/emotions.tsx
@@ -0,0 +1,361 @@
+import { useMemo, useState } from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Switch,
+ Text,
+ View,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { Button, Card, EmptyState, Loader, SectionTitle, TextField } from '@/components/ui';
+import { useAdminEmotions, useSaveEmotion, useDeleteEmotion } from '@/hooks/useAdmin';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme';
+import type { Emotion } from '@/types';
+
+interface FormState {
+ id?: number;
+ nom: string;
+ couleur: string;
+ icone: string;
+ niveau: number;
+ parent_id: number | null;
+ est_actif: boolean;
+}
+
+const FORM_VIDE: FormState = { nom: '', couleur: '#000091', icone: '', niveau: 1, parent_id: null, est_actif: true };
+
+function messageErreur(e: unknown, defaut: string): string {
+ const err = e as { response?: { data?: { message?: string } } };
+ return err?.response?.data?.message ?? defaut;
+}
+
+/** Regroupe une liste d'émotions (plate ou imbriquée) en niveau 1 + leurs enfants. */
+function organiser(emotions: Emotion[] | undefined): { parent: Emotion; enfants: Emotion[] }[] {
+ if (!emotions) return [];
+ const racines = emotions.filter((e) => e.niveau === 1);
+ return racines.map((parent) => {
+ const depuisEnfants = parent.enfants ?? [];
+ const depuisPlat = emotions.filter((e) => e.niveau === 2 && e.parent_id === parent.id);
+ // On dédoublonne par id au cas où les deux sources coexistent.
+ const map = new Map();
+ [...depuisEnfants, ...depuisPlat].forEach((e) => map.set(e.id, e));
+ return { parent, enfants: Array.from(map.values()) };
+ });
+}
+
+export default function AdminEmotionsScreen() {
+ const { data: emotions, isLoading } = useAdminEmotions();
+ const saveMut = useSaveEmotion();
+ const deleteMut = useDeleteEmotion();
+
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form, setForm] = useState(FORM_VIDE);
+
+ const groupes = useMemo(() => organiser(emotions), [emotions]);
+ const parents = useMemo(() => (emotions ?? []).filter((e) => e.niveau === 1), [emotions]);
+
+ const ouvrirCreation = () => {
+ setForm(FORM_VIDE);
+ setModalVisible(true);
+ };
+
+ const ouvrirEdition = (e: Emotion) => {
+ setForm({
+ id: e.id,
+ nom: e.nom,
+ couleur: e.couleur,
+ icone: e.icone ?? '',
+ niveau: e.niveau,
+ parent_id: e.parent_id,
+ est_actif: e.est_actif,
+ });
+ setModalVisible(true);
+ };
+
+ const soumettre = async () => {
+ if (!form.nom.trim()) {
+ Alert.alert('Champs requis', 'Le nom est obligatoire.');
+ return;
+ }
+ if (!/^#([0-9A-Fa-f]{6})$/.test(form.couleur.trim())) {
+ Alert.alert('Couleur', 'Saisissez une couleur hexadécimale (ex. #000091).');
+ return;
+ }
+ if (form.niveau === 2 && !form.parent_id) {
+ Alert.alert('Émotion parente', 'Choisissez l’émotion parente pour une sous-émotion.');
+ return;
+ }
+ try {
+ await saveMut.mutateAsync({
+ id: form.id,
+ nom: form.nom.trim(),
+ couleur: form.couleur.trim(),
+ icone: form.icone.trim() || null,
+ niveau: form.niveau,
+ parent_id: form.niveau === 2 ? form.parent_id : null,
+ est_actif: form.est_actif,
+ });
+ setModalVisible(false);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Enregistrement impossible.'));
+ }
+ };
+
+ const supprimer = (e: Emotion) => {
+ Alert.alert('Supprimer', `Supprimer « ${e.nom} » ?`, [
+ { text: 'Annuler', style: 'cancel' },
+ {
+ text: 'Supprimer',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await deleteMut.mutateAsync(e.id);
+ } catch (err) {
+ Alert.alert('Erreur', messageErreur(err, 'Suppression impossible.'));
+ }
+ },
+ },
+ ]);
+ };
+
+ const renderEmotion = (e: Emotion, estEnfant: boolean) => (
+
+
+
+ {e.icone ? {e.icone} : null}
+
+
+ {e.nom}
+
+ Niveau {e.niveau} · {e.est_actif ? 'Actif' : 'Inactif'} · {e.couleur}
+
+
+
+
+ ouvrirEdition(e)} />
+ supprimer(e)} />
+
+
+ );
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ {groupes.length === 0 ? (
+
+ ) : (
+ groupes.map(({ parent, enfants }) => (
+
+ {parent.nom}
+ {renderEmotion(parent, false)}
+ {enfants.map((enfant) => renderEmotion(enfant, true))}
+
+ ))
+ )}
+
+
+ setModalVisible(false)}>
+
+
+
+ {form.id ? 'Modifier l’émotion' : 'Nouvelle émotion'}
+ setModalVisible(false)} accessibilityLabel="Fermer">
+
+
+
+
+ setForm((f) => ({ ...f, nom: v }))}
+ required
+ autoCapitalize="words"
+ />
+
+
+ setForm((f) => ({ ...f, couleur: v }))}
+ placeholder="#000091"
+ required
+ />
+
+
+
+ setForm((f) => ({ ...f, icone: v }))}
+ placeholder="😊"
+ autoCapitalize="none"
+ />
+
+ Niveau
+
+ setForm((f) => ({ ...f, niveau: 1, parent_id: null }))}
+ >
+
+ 1 · Émotion
+
+
+ setForm((f) => ({ ...f, niveau: 2 }))}
+ >
+
+ 2 · Sous-émotion
+
+
+
+
+ {form.niveau === 2 ? (
+ <>
+ Émotion parente
+
+ {parents.length === 0 ? (
+ Aucune émotion de niveau 1 disponible.
+ ) : (
+ parents.map((p) => {
+ const actif = form.parent_id === p.id;
+ return (
+ setForm((f) => ({ ...f, parent_id: p.id }))}
+ >
+
+ {p.nom}
+
+ );
+ })
+ )}
+
+ >
+ ) : null}
+
+
+
+ Actif
+
+ {form.est_actif ? 'Disponible dans le tracker' : 'Masqué du tracker'}
+
+
+ setForm((f) => ({ ...f, est_actif: v }))}
+ trackColor={{ true: Colors.primary, false: Colors.gray[300] }}
+ thumbColor={Colors.white}
+ />
+
+
+
+ setModalVisible(false)} />
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: { flex: 1, backgroundColor: Colors.gray[50] },
+ list: { padding: Spacing.lg, gap: Spacing.md },
+ headerAction: { marginBottom: Spacing.md },
+ groupe: { gap: Spacing.sm, marginBottom: Spacing.md },
+ emotionCard: { padding: Spacing.md, gap: Spacing.sm },
+ emotionEnfant: { marginLeft: Spacing.xl, borderColor: Colors.gray[100] },
+ emotionRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
+ pastille: { width: 36, height: 36, borderRadius: Radius.pill, alignItems: 'center', justifyContent: 'center' },
+ pastilleIcone: { fontSize: 18 },
+ emotionInfo: { flex: 1 },
+ emotionNom: { fontSize: FontSize.md, fontWeight: FontWeight.semibold, color: Colors.black },
+ emotionMeta: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 },
+ actions: { flexDirection: 'row', gap: Spacing.sm },
+ modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' },
+ modalSheet: {
+ backgroundColor: Colors.white,
+ borderTopLeftRadius: Radius.md,
+ borderTopRightRadius: Radius.md,
+ maxHeight: '92%',
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: Spacing.lg,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray[200],
+ },
+ modalTitle: { fontSize: FontSize.lg, fontWeight: FontWeight.bold, color: Colors.black },
+ modalBody: { padding: Spacing.lg },
+ fieldLabel: { fontSize: FontSize.sm, fontWeight: FontWeight.medium, color: Colors.black, marginBottom: Spacing.xs },
+ couleurRow: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md },
+ couleurField: { flex: 1 },
+ couleurApercu: {
+ width: 44,
+ height: 44,
+ borderRadius: Radius.sm,
+ borderWidth: 1,
+ borderColor: Colors.gray[200],
+ marginTop: 24,
+ },
+ segment: {
+ flexDirection: 'row',
+ borderWidth: 1,
+ borderColor: Colors.primary,
+ borderRadius: Radius.sm,
+ overflow: 'hidden',
+ marginBottom: Spacing.lg,
+ },
+ segmentItem: { flex: 1, paddingVertical: Spacing.md, alignItems: 'center', backgroundColor: Colors.white },
+ segmentItemActive: { backgroundColor: Colors.primary },
+ segmentText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.primary },
+ segmentTextActive: { color: Colors.white },
+ parentList: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, marginBottom: Spacing.lg },
+ parentChip: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: Spacing.xs,
+ paddingVertical: Spacing.sm,
+ paddingHorizontal: Spacing.md,
+ borderRadius: Radius.sm,
+ borderWidth: 1,
+ borderColor: Colors.gray[300],
+ backgroundColor: Colors.white,
+ minHeight: 44,
+ },
+ parentChipActive: { borderColor: Colors.primary, backgroundColor: '#E3E3FD' },
+ parentDot: { width: 14, height: 14, borderRadius: Radius.pill },
+ parentChipText: { fontSize: FontSize.sm, color: Colors.gray[600] },
+ parentChipTextActive: { color: Colors.primary, fontWeight: FontWeight.semibold },
+ switchRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: Spacing.sm,
+ marginBottom: Spacing.md,
+ },
+ switchText: { flex: 1, paddingRight: Spacing.md },
+ switchHelper: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2 },
+ modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: Spacing.sm, marginTop: Spacing.sm },
+});
diff --git a/mobile/app/admin/index.tsx b/mobile/app/admin/index.tsx
new file mode 100644
index 0000000..2378884
--- /dev/null
+++ b/mobile/app/admin/index.tsx
@@ -0,0 +1,104 @@
+import { ScrollView, StyleSheet, Text, View } from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
+import { Card, SectionTitle } from '@/components/ui';
+import { useAdminUtilisateurs, useAdminFeeds, useAdminEmotions } from '@/hooks/useAdmin';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
+
+interface HubItem {
+ href: '/admin/utilisateurs' | '/admin/contenus' | '/admin/emotions';
+ icon: keyof typeof Ionicons.glyphMap;
+ titre: string;
+ sousTitre: string;
+ compteur?: number;
+}
+
+/** Hub d'administration : accès aux trois sections de gestion. */
+export default function AdminHubScreen() {
+ const router = useRouter();
+ const { data: utilisateurs } = useAdminUtilisateurs();
+ const { data: feeds } = useAdminFeeds();
+ const { data: emotions } = useAdminEmotions();
+
+ const items: HubItem[] = [
+ {
+ href: '/admin/utilisateurs',
+ icon: 'people-outline',
+ titre: 'Utilisateurs',
+ sousTitre: 'Comptes, rôles et activation',
+ compteur: utilisateurs?.length,
+ },
+ {
+ href: '/admin/contenus',
+ icon: 'newspaper-outline',
+ titre: 'Contenus',
+ sousTitre: 'Articles d’information santé mentale',
+ compteur: feeds?.length,
+ },
+ {
+ href: '/admin/emotions',
+ icon: 'color-palette-outline',
+ titre: 'Émotions',
+ sousTitre: 'Référentiel des émotions et sous-émotions',
+ compteur: emotions?.length,
+ },
+ ];
+
+ return (
+
+ Espace d’administration
+ Gérez les utilisateurs, les contenus et le référentiel des émotions.
+
+
+ {items.map((item) => (
+ router.push(item.href)} style={styles.card}>
+
+
+
+
+
+ {item.titre}
+ {item.sousTitre}
+
+
+ {typeof item.compteur === 'number' ? (
+ {item.compteur}
+ ) : null}
+
+
+
+
+ ))}
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: { flex: 1, backgroundColor: Colors.gray[50] },
+ content: { padding: Spacing.lg, gap: Spacing.md },
+ intro: { fontSize: FontSize.sm, color: Colors.gray[500], marginBottom: Spacing.sm, lineHeight: 20 },
+ list: { gap: Spacing.md },
+ card: { padding: Spacing.lg },
+ row: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
+ iconCircle: {
+ width: 48,
+ height: 48,
+ borderRadius: Radius.md,
+ backgroundColor: '#E3E3FD',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ cardBody: { flex: 1 },
+ cardTitle: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.black },
+ cardSubtitle: { fontSize: FontSize.xs, color: Colors.gray[500], marginTop: 2, lineHeight: 18 },
+ cardEnd: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm },
+ count: {
+ fontSize: FontSize.sm,
+ fontWeight: FontWeight.bold,
+ color: Colors.primary,
+ minWidth: 22,
+ textAlign: 'right',
+ },
+});
diff --git a/mobile/app/admin/utilisateurs.tsx b/mobile/app/admin/utilisateurs.tsx
new file mode 100644
index 0000000..eec369e
--- /dev/null
+++ b/mobile/app/admin/utilisateurs.tsx
@@ -0,0 +1,268 @@
+import { useState } from 'react';
+import {
+ Alert,
+ FlatList,
+ KeyboardAvoidingView,
+ Modal,
+ Platform,
+ Pressable,
+ ScrollView,
+ StyleSheet,
+ Text,
+ View,
+} from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
+import { Badge, Button, Card, EmptyState, Loader, TextField } from '@/components/ui';
+import {
+ useAdminUtilisateurs,
+ useCreateUtilisateur,
+ useToggleUtilisateurActif,
+ useDeleteUtilisateur,
+} from '@/hooks/useAdmin';
+import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, HIT_SLOP, Radius, Spacing } from '@/lib/theme';
+import type { Utilisateur } from '@/types';
+
+const ROLE_MEMBRE = 2;
+const ROLE_ADMIN = 3;
+
+interface FormState {
+ prenom: string;
+ nom: string;
+ email: string;
+ password: string;
+ password_confirmation: string;
+ role_id: number;
+}
+
+const FORM_VIDE: FormState = {
+ prenom: '',
+ nom: '',
+ email: '',
+ password: '',
+ password_confirmation: '',
+ role_id: ROLE_MEMBRE,
+};
+
+/** Extrait le message d'erreur d'une réponse API (422 backend) ou un texte par défaut. */
+function messageErreur(e: unknown, defaut: string): string {
+ const err = e as { response?: { data?: { message?: string } } };
+ return err?.response?.data?.message ?? defaut;
+}
+
+export default function AdminUtilisateursScreen() {
+ const { data: utilisateurs, isLoading } = useAdminUtilisateurs();
+ const createMut = useCreateUtilisateur();
+ const toggleMut = useToggleUtilisateurActif();
+ const deleteMut = useDeleteUtilisateur();
+
+ const [modalVisible, setModalVisible] = useState(false);
+ const [form, setForm] = useState(FORM_VIDE);
+
+ const ouvrirFormulaire = () => {
+ setForm(FORM_VIDE);
+ setModalVisible(true);
+ };
+
+ const set = (cle: keyof FormState) => (valeur: string) => setForm((f) => ({ ...f, [cle]: valeur }));
+
+ const soumettre = async () => {
+ if (!form.prenom.trim() || !form.nom.trim() || !form.email.trim() || !form.password) {
+ Alert.alert('Champs requis', 'Merci de renseigner tous les champs.');
+ return;
+ }
+ if (form.password !== form.password_confirmation) {
+ Alert.alert('Mots de passe', 'La confirmation ne correspond pas.');
+ return;
+ }
+ try {
+ await createMut.mutateAsync(form);
+ setModalVisible(false);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Impossible de créer l’utilisateur.'));
+ }
+ };
+
+ const basculerActif = async (u: Utilisateur) => {
+ try {
+ await toggleMut.mutateAsync(u.id);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Action impossible.'));
+ }
+ };
+
+ const supprimer = (u: Utilisateur) => {
+ Alert.alert('Supprimer', `Supprimer ${u.prenom} ${u.nom} ?`, [
+ { text: 'Annuler', style: 'cancel' },
+ {
+ text: 'Supprimer',
+ style: 'destructive',
+ onPress: async () => {
+ try {
+ await deleteMut.mutateAsync(u.id);
+ } catch (e) {
+ Alert.alert('Erreur', messageErreur(e, 'Suppression impossible.'));
+ }
+ },
+ },
+ ]);
+ };
+
+ const renderItem = ({ item }: { item: Utilisateur }) => (
+
+
+
+
+ {item.prenom} {item.nom}
+
+ {item.email}
+
+
+
+
+
+
+
+ basculerActif(item)}
+ />
+ supprimer(item)} />
+
+
+ );
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+ String(item.id)}
+ renderItem={renderItem}
+ contentContainerStyle={styles.list}
+ ListHeaderComponent={
+
+
+
+ }
+ ListEmptyComponent={
+
+ }
+ />
+
+ setModalVisible(false)}>
+
+
+
+ Nouvel utilisateur
+ setModalVisible(false)} accessibilityLabel="Fermer">
+
+
+
+
+
+
+
+
+
+
+ Rôle
+
+ setForm((f) => ({ ...f, role_id: ROLE_MEMBRE }))}
+ >
+
+ Membre
+
+
+ setForm((f) => ({ ...f, role_id: ROLE_ADMIN }))}
+ >
+
+ Administrateur
+
+
+
+
+
+ setModalVisible(false)} />
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ screen: { flex: 1, backgroundColor: Colors.gray[50] },
+ list: { padding: Spacing.lg, gap: Spacing.md },
+ headerAction: { marginBottom: Spacing.md },
+ card: { padding: Spacing.lg, gap: Spacing.sm },
+ cardHeader: { flexDirection: 'row', alignItems: 'flex-start' },
+ cardHeaderText: { flex: 1 },
+ nom: { fontSize: FontSize.md, fontWeight: FontWeight.bold, color: Colors.black },
+ email: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: 2 },
+ badges: { flexDirection: 'row', gap: Spacing.sm, flexWrap: 'wrap' },
+ actions: { flexDirection: 'row', gap: Spacing.sm, marginTop: Spacing.xs },
+ modalOverlay: { flex: 1, justifyContent: 'flex-end', backgroundColor: 'rgba(22,22,22,0.4)' },
+ modalSheet: {
+ backgroundColor: Colors.white,
+ borderTopLeftRadius: Radius.md,
+ borderTopRightRadius: Radius.md,
+ maxHeight: '90%',
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ padding: Spacing.lg,
+ borderBottomWidth: 1,
+ borderBottomColor: Colors.gray[200],
+ },
+ modalTitle: { fontSize: FontSize.lg, fontWeight: FontWeight.bold, color: Colors.black },
+ modalBody: { padding: Spacing.lg },
+ fieldLabel: { fontSize: FontSize.sm, fontWeight: FontWeight.medium, color: Colors.black, marginBottom: Spacing.xs },
+ segment: {
+ flexDirection: 'row',
+ borderWidth: 1,
+ borderColor: Colors.primary,
+ borderRadius: Radius.sm,
+ overflow: 'hidden',
+ marginBottom: Spacing.lg,
+ },
+ segmentItem: { flex: 1, paddingVertical: Spacing.md, alignItems: 'center', backgroundColor: Colors.white },
+ segmentItemActive: { backgroundColor: Colors.primary },
+ segmentText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.primary },
+ segmentTextActive: { color: Colors.white },
+ modalActions: { flexDirection: 'row', justifyContent: 'flex-end', gap: Spacing.sm, marginTop: Spacing.sm },
+});
diff --git a/mobile/app/auth/forgot-password.tsx b/mobile/app/auth/forgot-password.tsx
index 36aad47..42d6ca1 100644
--- a/mobile/app/auth/forgot-password.tsx
+++ b/mobile/app/auth/forgot-password.tsx
@@ -1,8 +1,10 @@
import { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
-import { Link, useRouter } from 'expo-router';
+import { View, Text, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
+import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { Colors } from '@/lib/colors';
+import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
+import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -28,20 +30,27 @@ export default function ForgotPasswordScreen() {
if (submitted) {
return (
-
-
+
+
+
- Demande enregistrée
+ Vérifiez votre boîte mail
- Si l'adresse {email} est associée à un compte CESIZen, un email contenant les instructions de réinitialisation sera envoyé.
+ Si l'adresse {email} est associée à un compte CESIZen,
+ un email contenant les instructions de réinitialisation sera envoyé.
Si vous ne recevez rien sous 24h, contactez votre administrateur.
- router.replace('/auth/login')}>
- Retour à la connexion
-
+
+ router.replace('/auth/login')}
+ fullWidth
+ />
+
);
@@ -49,64 +58,64 @@ export default function ForgotPasswordScreen() {
return (
+
-
- Mot de passe oublié ?
-
- Entrez votre adresse email. Si elle est associée à un compte, vous recevrez les instructions pour réinitialiser votre mot de passe.
-
-
+
+ Entrez votre adresse email. Si elle est associée à un compte, vous recevrez les
+ instructions pour réinitialiser votre mot de passe.
+
-
-
- Email
- { setEmail(v); setError(null); }}
- placeholder="votre@email.com"
- keyboardType="email-address"
- autoCapitalize="none"
- autoCorrect={false}
+
+ { setEmail(v); setError(null); }}
+ placeholder="votre@email.com"
+ keyboardType="email-address"
+ autoCapitalize="none"
+ autoComplete="email"
+ error={error ?? undefined}
+ required
+ />
+
+
+
+
+ router.replace('/auth/login')}
/>
- {error && {error}}
-
-
- Envoyer les instructions
-
-
-
-
-
- Retour à la connexion
-
-
-
-
+
);
}
const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: Colors.white },
- scrollContent: { flexGrow: 1, justifyContent: 'center', padding: 24 },
- header: { alignItems: 'center', marginBottom: 32 },
- logo: { fontSize: 26, fontWeight: '800', color: Colors.black, textAlign: 'center' },
- subtitle: { fontSize: 14, color: Colors.gray[500], marginTop: 10, textAlign: 'center', lineHeight: 20 },
- form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
- inputGroup: { marginBottom: 16 },
- label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
- input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
- errorText: { color: Colors.error, marginTop: 6, fontSize: 12 },
- button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 8 },
- buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
- footer: { alignItems: 'center', marginTop: 20 },
- link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
- success: { padding: 24, alignItems: 'center', flex: 1, justifyContent: 'center' },
- successIcon: { width: 72, height: 72, borderRadius: 999, backgroundColor: `${Colors.primary}15`, justifyContent: 'center', alignItems: 'center', marginBottom: 20 },
- successTitle: { fontSize: 22, fontWeight: '800', color: Colors.black, marginBottom: 12 },
- successText: { fontSize: 14, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: 12 },
- bold: { fontWeight: '700', color: Colors.black },
+ container: { flex: 1, backgroundColor: Colors.gray[50] },
+ scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
+ intro: { fontSize: FontSize.sm, color: Colors.gray[600], lineHeight: 20, marginBottom: Spacing.xl, textAlign: 'center' },
+ backRow: { alignItems: 'center', marginTop: Spacing.sm },
+ successWrap: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: Spacing.xl },
+ iconCircle: {
+ width: 72,
+ height: 72,
+ borderRadius: Radius.pill,
+ backgroundColor: '#E3E3FD',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginBottom: Spacing.lg,
+ },
+ successTitle: { fontSize: FontSize.xl, fontWeight: FontWeight.heavy, color: Colors.black, marginBottom: Spacing.md, textAlign: 'center' },
+ successText: { fontSize: FontSize.sm, color: Colors.gray[600], textAlign: 'center', lineHeight: 20, marginBottom: Spacing.md },
+ bold: { fontWeight: FontWeight.bold, color: Colors.black },
+ successAction: { alignSelf: 'stretch', marginTop: Spacing.lg },
});
diff --git a/mobile/app/auth/login.tsx b/mobile/app/auth/login.tsx
index c9aaf92..31fe20f 100644
--- a/mobile/app/auth/login.tsx
+++ b/mobile/app/auth/login.tsx
@@ -1,10 +1,12 @@
import { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
-import { Link, useRouter } from 'expo-router';
+import { View, Text, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
+import { useRouter } from 'expo-router';
+import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
-import RepubliqueHeader from '@/components/RepubliqueHeader';
+import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
+import { AppBar, Button, Card, TextField } from '@/components/ui';
export default function LoginScreen() {
const router = useRouter();
@@ -12,18 +14,22 @@ export default function LoginScreen() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
const handleLogin = async () => {
+ setError(null);
if (!email || !password) {
- Alert.alert('Erreur', 'Veuillez remplir tous les champs');
+ setError('Veuillez remplir tous les champs.');
return;
}
setLoading(true);
try {
await login(email, password);
router.replace('/(tabs)');
- } catch (error) {
- Alert.alert('Erreur', getApiError(error).message || 'Identifiants incorrects');
+ } catch (err) {
+ const message = getApiError(err).message || 'Identifiants incorrects';
+ setError(message);
+ Alert.alert('Erreur', message);
} finally {
setLoading(false);
}
@@ -31,62 +37,69 @@ export default function LoginScreen() {
return (
-
+
-
- CESIZen
- Votre compagnon de santé mentale
+
+ Bienvenue sur CESIZen
+ Votre compagnon de santé mentale.
-
- Connexion
+
+ {error ? (
+
+
+ {error}
+
+ ) : null}
-
- Email
-
-
+ { setEmail(v); setError(null); }}
+ placeholder="votre@email.com"
+ keyboardType="email-address"
+ autoCapitalize="none"
+ autoComplete="email"
+ required
+ />
-
- Mot de passe
-
-
+ { setPassword(v); setError(null); }}
+ placeholder="Votre mot de passe"
+ secureTextEntry
+ autoComplete="password"
+ required
+ />
-
- {loading ? 'Connexion...' : 'Se connecter'}
-
+ loading={loading}
+ icon="log-in-outline"
+ fullWidth
+ />
-
-
- Mot de passe oublié ?
-
-
-
-
- Pas encore de compte ?
-
-
- S'inscrire
-
-
+
+ router.push('/auth/forgot-password')}
+ />
+
+
+
+ Pas encore de compte ?
+ router.push('/auth/register')}
+ icon="person-add-outline"
+ fullWidth
+ />
@@ -94,37 +107,24 @@ export default function LoginScreen() {
}
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.primary },
- subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
- form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
- title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
- inputGroup: { marginBottom: 16 },
- label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
- input: {
- backgroundColor: Colors.gray[100],
- borderTopLeftRadius: 4,
- borderTopRightRadius: 4,
- borderBottomWidth: 2,
- borderBottomColor: Colors.black,
- paddingHorizontal: 16,
- paddingVertical: 14,
- fontSize: 16,
- color: Colors.black,
- },
- button: {
- backgroundColor: Colors.primary,
- borderRadius: 4,
- paddingVertical: 16,
+ container: { flex: 1, backgroundColor: Colors.gray[50] },
+ scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
+ intro: { alignItems: 'center', marginBottom: Spacing.xl },
+ welcome: { fontSize: FontSize.xxl, fontWeight: FontWeight.heavy, color: Colors.black, textAlign: 'center' },
+ welcomeSub: { fontSize: FontSize.sm, color: Colors.gray[500], marginTop: Spacing.xs, textAlign: 'center' },
+ errorBanner: {
+ flexDirection: 'row',
alignItems: 'center',
- marginTop: 8,
+ gap: Spacing.sm,
+ backgroundColor: Colors.errorBg,
+ borderRadius: Radius.sm,
+ borderLeftWidth: 3,
+ borderLeftColor: Colors.error,
+ padding: Spacing.md,
+ marginBottom: Spacing.lg,
},
- buttonDisabled: { opacity: 0.6 },
- buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
- forgotLink: { alignItems: 'center', marginTop: 12 },
- footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 16 },
- footerText: { color: Colors.gray[500], fontSize: 14 },
- link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
+ errorBannerText: { flex: 1, fontSize: FontSize.xs, color: Colors.error, fontWeight: FontWeight.medium },
+ forgotRow: { alignItems: 'center', marginTop: Spacing.sm },
+ footer: { marginTop: Spacing.xl, alignItems: 'center' },
+ footerText: { fontSize: FontSize.sm, color: Colors.gray[500], marginBottom: Spacing.sm },
});
diff --git a/mobile/app/auth/register.tsx b/mobile/app/auth/register.tsx
index 137ed19..a928ded 100644
--- a/mobile/app/auth/register.tsx
+++ b/mobile/app/auth/register.tsx
@@ -1,11 +1,12 @@
import { useState } from 'react';
-import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
-import { Link, useRouter } from 'expo-router';
+import { View, Text, StyleSheet, Alert, KeyboardAvoidingView, Platform, ScrollView, Pressable } from 'react-native';
+import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useAuthStore } from '@/lib/auth-store';
import { getApiError } from '@/lib/api';
import { Colors } from '@/lib/colors';
-import RepubliqueHeader from '@/components/RepubliqueHeader';
+import { FontSize, FontWeight, MIN_TOUCH, Radius, Spacing } from '@/lib/theme';
+import { AppBar, Button, Card, TextField } from '@/components/ui';
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -60,70 +61,112 @@ export default function RegisterScreen() {
}
};
+ const consentError = errors.consentement || errors.consentement_rgpd;
+
return (
-
+
-
- CESIZen
- Créez votre compte
-
-
-
- Inscription
-
-
-
- Nom
-
- {errors.nom && {errors.nom}}
-
-
- Prénom
-
- {errors.prenom && {errors.prenom}}
-
+
+
+
+
-
- Email
-
- {errors.email && {errors.email}}
-
+
-
- Mot de passe (min. 8 caractères)
-
- {errors.password && {errors.password}}
-
+
-
- Confirmer le mot de passe
-
- {errors.passwordConfirmation && {errors.passwordConfirmation}}
-
+
- setConsentement(!consentement)} activeOpacity={0.7}>
-
- {consentement && }
-
+ setConsentement(!consentement)}
+ accessibilityRole="checkbox"
+ accessibilityState={{ checked: consentement }}
+ >
+
- J'accepte le traitement de mes données de bien-être conformément à la politique de confidentialité (RGPD).
+ J'accepte le traitement de mes données de bien-être conformément à la{' '}
+ politique de confidentialité (RGPD).
-
- {errors.consentement && {errors.consentement}}
- {errors.consentement_rgpd && {errors.consentement_rgpd}}
+
+ {consentError ? (
+
+
+ {consentError}
+
+ ) : null}
-
- {loading ? 'Inscription...' : 'S\'inscrire'}
-
-
-
- Déjà un compte ?
-
- Se connecter
-
+
+
+
+
+
+ Déjà un compte ?
+ router.push('/auth/login')}
+ />
@@ -131,27 +174,23 @@ export default function RegisterScreen() {
}
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.primary },
- subtitle: { fontSize: 16, color: Colors.gray[500], marginTop: 8 },
- form: { backgroundColor: Colors.gray[50], borderRadius: 4, padding: 24, borderWidth: 1, borderColor: Colors.gray[200] },
- title: { fontSize: 24, fontWeight: '700', color: Colors.black, marginBottom: 24 },
- row: { flexDirection: 'row' },
- inputGroup: { marginBottom: 16 },
- label: { fontSize: 14, fontWeight: '600', color: Colors.black, marginBottom: 6 },
- input: { backgroundColor: Colors.gray[100], borderTopLeftRadius: 4, borderTopRightRadius: 4, borderBottomWidth: 2, borderBottomColor: Colors.black, paddingHorizontal: 16, paddingVertical: 14, fontSize: 16, color: Colors.black },
- errorText: { color: Colors.error, marginTop: 4, fontSize: 12 },
- consentRow: { flexDirection: 'row', alignItems: 'flex-start', gap: 10, marginVertical: 12 },
- checkbox: { width: 22, height: 22, borderRadius: 4, borderWidth: 2, borderColor: Colors.black, backgroundColor: Colors.white, justifyContent: 'center', alignItems: 'center', marginTop: 1 },
- checkboxChecked: { backgroundColor: Colors.primary, borderColor: Colors.primary },
- consentText: { flex: 1, fontSize: 13, color: Colors.black, lineHeight: 18 },
- consentLink: { color: Colors.primary, fontWeight: '600', textDecorationLine: 'underline' },
- button: { backgroundColor: Colors.primary, borderRadius: 4, paddingVertical: 16, alignItems: 'center', marginTop: 12 },
- buttonDisabled: { opacity: 0.6 },
- buttonText: { fontSize: 16, fontWeight: '700', color: Colors.white },
- footer: { flexDirection: 'row', justifyContent: 'center', marginTop: 20 },
- footerText: { color: Colors.gray[500], fontSize: 14 },
- link: { color: Colors.primary, fontSize: 14, fontWeight: '600', textDecorationLine: 'underline' },
+ container: { flex: 1, backgroundColor: Colors.gray[50] },
+ scrollContent: { flexGrow: 1, justifyContent: 'center', padding: Spacing.xl },
+ nameRow: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md },
+ nameField: { flexGrow: 1, flexBasis: 130 },
+ consentRow: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
+ gap: Spacing.sm,
+ paddingVertical: Spacing.sm,
+ minHeight: MIN_TOUCH,
+ },
+ consentIcon: { marginTop: 1 },
+ consentText: { flex: 1, fontSize: FontSize.xs, color: Colors.black, lineHeight: 19 },
+ consentLink: { color: Colors.primary, fontWeight: FontWeight.semibold, textDecorationLine: 'underline' },
+ consentErrorRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginTop: Spacing.xs },
+ consentErrorText: { fontSize: FontSize.xs, color: Colors.error },
+ submit: { marginTop: Spacing.lg },
+ footer: { marginTop: Spacing.xl, alignItems: 'center' },
+ footerText: { fontSize: FontSize.sm, color: Colors.gray[500] },
});
diff --git a/mobile/components/AdminGuard.tsx b/mobile/components/AdminGuard.tsx
new file mode 100644
index 0000000..f5a1d85
--- /dev/null
+++ b/mobile/components/AdminGuard.tsx
@@ -0,0 +1,27 @@
+import { useEffect } from 'react';
+import { useRouter } from 'expo-router';
+import { useAuthStore } from '@/lib/auth-store';
+
+/**
+ * Garde d'affichage pour les écrans d'administration.
+ * Si l'utilisateur connecté n'est pas administrateur, on le renvoie vers
+ * l'espace principal. La sécurité réelle reste assurée côté serveur (middleware
+ * role:administrateur) ; ce composant évite simplement d'afficher l'UI admin.
+ */
+export default function AdminGuard({ children }: { children: React.ReactNode }) {
+ const router = useRouter();
+ const user = useAuthStore((state) => state.user);
+ const estAdmin = user?.role?.nom === 'administrateur';
+
+ useEffect(() => {
+ if (!estAdmin) {
+ router.replace('/(tabs)');
+ }
+ }, [estAdmin, router]);
+
+ if (!estAdmin) {
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/mobile/components/EmotionPicker.tsx b/mobile/components/EmotionPicker.tsx
index d6bcc53..58f5801 100644
--- a/mobile/components/EmotionPicker.tsx
+++ b/mobile/components/EmotionPicker.tsx
@@ -1,7 +1,10 @@
import { useState } from 'react';
-import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native';
+import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
+import { Ionicons } from '@expo/vector-icons';
import { useEmotions } from '@/hooks/useEmotions';
import { Colors } from '@/lib/colors';
+import { Spacing, Radius, FontSize, FontWeight, MIN_TOUCH, HIT_SLOP } from '@/lib/theme';
+import { Loader } from '@/components/ui';
import type { Emotion } from '@/types';
interface EmotionPickerProps {
@@ -14,40 +17,29 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
const [selectedParent, setSelectedParent] = useState(null);
if (isLoading) {
- return (
-
-
-
- );
+ return ;
}
if (selectedParent?.enfants && selectedParent.enfants.length > 0) {
return (
- setSelectedParent(null)}>
- ← Retour aux émotions
+ setSelectedParent(null)}>
+
+ Retour aux émotions
Précisez votre émotion de{' '}
- {selectedParent.nom} :
+ {selectedParent.nom} :
- {selectedParent.enfants.map((child) => {
- const isSelected = value === child.id;
- return (
- onChange(child.id)}
- >
- {child.icone || '🔵'}
- {child.nom}
-
- );
- })}
+ {selectedParent.enfants.map((child) => (
+ onChange(child.id)}
+ />
+ ))}
);
@@ -58,14 +50,12 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
{emotions?.map((emotion) => {
const hasChildren = emotion.enfants && emotion.enfants.length > 0;
const isSelected = value === emotion.id || emotion.enfants?.some((e) => e.id === value);
-
return (
- {
if (hasChildren) {
setSelectedParent(emotion);
@@ -73,34 +63,62 @@ export default function EmotionPicker({ value, onChange }: EmotionPickerProps) {
onChange(emotion.id);
}
}}
- >
- {emotion.icone || '🔵'}
- {emotion.nom}
- {hasChildren && ▼}
-
+ />
);
})}
);
}
+function EmotionTile({
+ emotion,
+ selected,
+ hasChildren,
+ onPress,
+}: {
+ emotion: Emotion;
+ selected: boolean;
+ hasChildren?: boolean;
+ onPress: () => void;
+}) {
+ return (
+
+ {emotion.icone || '🔵'}
+
+ {emotion.nom}
+
+ {hasChildren && }
+
+ );
+}
+
const styles = StyleSheet.create({
- loading: { padding: 40, alignItems: 'center' },
- backBtn: { marginBottom: 12 },
- backText: { fontSize: 14, color: Colors.primary, textDecorationLine: 'underline' },
- subTitle: { fontSize: 14, color: Colors.gray[600], marginBottom: 12 },
- grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
- emotionCard: {
+ backBtn: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginBottom: Spacing.md, minHeight: MIN_TOUCH },
+ backText: { fontSize: FontSize.sm, fontWeight: FontWeight.semibold, color: Colors.primary },
+ subTitle: { fontSize: FontSize.sm, color: Colors.gray[600], marginBottom: Spacing.md },
+ subTitleAccent: { fontWeight: FontWeight.bold },
+ grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.md, justifyContent: 'space-between' },
+ tile: {
width: '30%',
+ minHeight: 92,
backgroundColor: Colors.white,
- borderRadius: 4,
- padding: 14,
+ borderRadius: Radius.sm,
+ paddingVertical: Spacing.md,
+ paddingHorizontal: Spacing.sm,
alignItems: 'center',
- gap: 6,
+ justifyContent: 'center',
+ gap: Spacing.xs,
borderWidth: 1.5,
borderColor: Colors.gray[200],
},
- emotionEmoji: { fontSize: 30 },
- emotionName: { fontSize: 12, fontWeight: '600', color: Colors.black, textAlign: 'center' },
- arrow: { fontSize: 10, color: Colors.gray[400] },
+ tileEmoji: { fontSize: 30 },
+ tileName: { fontSize: FontSize.xs, fontWeight: FontWeight.semibold, color: Colors.black, textAlign: 'center' },
+ chevron: { marginTop: 2 },
});