feat(mobile): refonte design DSFR complète + partie admin
- écrans auth/dashboard/journal/info/urgence/profil refondus sur le design system - AppBar institutionnel République Française sur tous les écrans - partie admin (utilisateurs, contenus, émotions) avec AdminGuard - entrée Espace administration dans le profil (admins uniquement) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { View, Text, StyleSheet, ScrollView, ActivityIndicator } from 'react-native';
|
||||
import { View, Text, StyleSheet, ScrollView } from 'react-native';
|
||||
import { useLocalSearchParams, Stack } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useFeed } from '@/hooks/useFeeds';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import { FontSize, FontWeight, Spacing } from '@/lib/theme';
|
||||
import { Badge, EmptyState, Loader } from '@/components/ui';
|
||||
|
||||
export default function FeedDetailScreen() {
|
||||
const { slug } = useLocalSearchParams<{ slug: string }>();
|
||||
@@ -10,17 +11,22 @@ export default function FeedDetailScreen() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.primary} />
|
||||
<View style={styles.fill}>
|
||||
<Stack.Screen options={{ title: 'Article' }} />
|
||||
<Loader label="Chargement de l'article…" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !feed) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Ionicons name="alert-circle-outline" size={48} color={Colors.gray[400]} />
|
||||
<Text style={styles.errorText}>Article introuvable</Text>
|
||||
<View style={styles.fill}>
|
||||
<Stack.Screen options={{ title: 'Article' }} />
|
||||
<EmptyState
|
||||
icon="alert-circle-outline"
|
||||
title="Article introuvable"
|
||||
message="Cet article n'existe plus ou n'est pas accessible."
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -28,34 +34,40 @@ export default function FeedDetailScreen() {
|
||||
const texteNettoye = (feed.contenu ?? '').replace(/<[^>]*>/g, '');
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen options={{ title: feed.titre ?? '' }} />
|
||||
<View style={styles.fill}>
|
||||
<Stack.Screen options={{ title: feed.titre ?? 'Article' }} />
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.badge}>
|
||||
<Ionicons name="information-circle" size={14} color={Colors.primary} />
|
||||
<Text style={styles.badgeText}>Information santé mentale</Text>
|
||||
</View>
|
||||
<Badge label="Information santé mentale" tone="primary" />
|
||||
|
||||
<Text style={styles.title}>{feed.titre}</Text>
|
||||
|
||||
<Text style={styles.date}>
|
||||
Publié le {new Date(feed.created_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric' })}
|
||||
Publié le{' '}
|
||||
{new Date(feed.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Text style={styles.body}>{texteNettoye}</Text>
|
||||
</ScrollView>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
content: { padding: 20, paddingBottom: 40 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: Colors.white },
|
||||
badge: { flexDirection: 'row', alignItems: 'center', gap: 6, alignSelf: 'flex-start', backgroundColor: `${Colors.primary}15`, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, marginBottom: 12 },
|
||||
badgeText: { fontSize: 12, fontWeight: '600', color: Colors.primary },
|
||||
title: { fontSize: 24, fontWeight: '800', color: Colors.black, marginBottom: 8, lineHeight: 30 },
|
||||
date: { fontSize: 13, color: Colors.gray[400], marginBottom: 20 },
|
||||
body: { fontSize: 15, lineHeight: 24, color: Colors.black },
|
||||
errorText: { marginTop: 12, fontSize: 16, color: Colors.gray[400] },
|
||||
fill: { flex: 1, backgroundColor: Colors.gray[50] },
|
||||
container: { flex: 1, backgroundColor: Colors.gray[50] },
|
||||
content: { padding: Spacing.xl, paddingBottom: Spacing.xxxl },
|
||||
title: {
|
||||
fontSize: FontSize.xxl,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
marginTop: Spacing.md,
|
||||
marginBottom: Spacing.sm,
|
||||
lineHeight: 30,
|
||||
},
|
||||
date: { fontSize: FontSize.xs, color: Colors.gray[400], marginBottom: Spacing.xl },
|
||||
body: { fontSize: FontSize.body, lineHeight: 24, color: Colors.black },
|
||||
});
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Stack } from 'expo-router';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import { FontWeight } from '@/lib/theme';
|
||||
|
||||
export default function InfoLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: Colors.white },
|
||||
headerTitleStyle: { fontWeight: '700', color: Colors.black },
|
||||
headerTitleStyle: { fontWeight: FontWeight.bold, color: Colors.black },
|
||||
headerTintColor: Colors.primary,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ title: 'Infos' }} />
|
||||
<Stack.Screen name="[slug]" options={{ title: '', headerBackTitle: 'Retour' }} />
|
||||
{/* La liste rend son propre <AppBar/>, on masque le header natif. */}
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
{/* Le détail garde un header natif avec bouton retour. */}
|
||||
<Stack.Screen name="[slug]" options={{ title: 'Article', headerBackTitle: 'Retour' }} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,81 +1,84 @@
|
||||
import { View, Text, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
||||
import { View, Text, StyleSheet, FlatList } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { useFeeds } from '@/hooks/useFeeds';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import { FontSize, FontWeight, Radius, Spacing } from '@/lib/theme';
|
||||
import { AppBar, Card, EmptyState, Loader } from '@/components/ui';
|
||||
import type { Feed } from '@/types';
|
||||
|
||||
export default function InfoListScreen() {
|
||||
const { data: feeds, isLoading } = useFeeds();
|
||||
const router = useRouter();
|
||||
|
||||
const renderItem = ({ item }: { item: Feed }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.card}
|
||||
onPress={() => router.push(`/(tabs)/info/${item.slug}`)}
|
||||
>
|
||||
<View style={styles.cardIcon}>
|
||||
<Ionicons name="document-text" size={24} color={Colors.primary} />
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardTitle}>{item.titre}</Text>
|
||||
<Text style={styles.cardExcerpt} numberOfLines={2}>
|
||||
{item.contenu?.replace(/<[^>]*>/g, '').substring(0, 120)}...
|
||||
</Text>
|
||||
<Text style={styles.cardDate}>
|
||||
{new Date(item.created_at).toLocaleDateString('fr-FR')}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color={Colors.gray[300]} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
const renderItem = ({ item }: { item: Feed }) => {
|
||||
const extrait = (item.contenu ?? '').replace(/<[^>]*>/g, '').trim();
|
||||
return (
|
||||
<Card onPress={() => router.push(`/(tabs)/info/${item.slug}`)} style={styles.card}>
|
||||
<View style={styles.cardRow}>
|
||||
<View style={styles.cardIcon}>
|
||||
<Ionicons name="document-text" size={24} color={Colors.primary} />
|
||||
</View>
|
||||
<View style={styles.cardContent}>
|
||||
<Text style={styles.cardTitle}>{item.titre}</Text>
|
||||
{extrait ? (
|
||||
<Text style={styles.cardExcerpt} numberOfLines={2}>
|
||||
{extrait}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.cardDate}>
|
||||
{new Date(item.created_at).toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color={Colors.gray[400]} />
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={feeds || []}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Santé mentale</Text>
|
||||
<Text style={styles.headerSubtitle}>Ressources et informations pour votre bien-être</Text>
|
||||
</View>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
isLoading ? (
|
||||
<View style={styles.loading}><Text style={styles.loadingText}>Chargement...</Text></View>
|
||||
) : (
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="newspaper-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyText}>Aucun article disponible</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<AppBar title="Informations" subtitle="Ressources pour votre santé mentale" />
|
||||
{isLoading ? (
|
||||
<Loader label="Chargement des articles…" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={feeds ?? []}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
icon="newspaper-outline"
|
||||
title="Aucun article disponible"
|
||||
message="Les ressources d'information apparaîtront ici dès leur publication."
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
listContent: { padding: 16 },
|
||||
header: { marginBottom: 20 },
|
||||
headerTitle: { fontSize: 14, fontWeight: '700', color: Colors.primary, textTransform: 'uppercase', letterSpacing: 1 },
|
||||
headerSubtitle: { fontSize: 15, color: Colors.gray[500], marginTop: 4 },
|
||||
card: {
|
||||
flexDirection: 'row', alignItems: 'center', gap: 14, padding: 16,
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
container: { flex: 1, backgroundColor: Colors.gray[50] },
|
||||
listContent: { padding: Spacing.lg, gap: Spacing.md },
|
||||
card: { padding: Spacing.lg },
|
||||
cardRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md },
|
||||
cardIcon: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: Radius.sm,
|
||||
backgroundColor: '#E3E3FD',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cardIcon: { width: 48, height: 48, borderRadius: 4, backgroundColor: `${Colors.primary}15`, justifyContent: 'center', alignItems: 'center' },
|
||||
cardContent: { flex: 1 },
|
||||
cardTitle: { fontSize: 15, fontWeight: '700', color: Colors.black, marginBottom: 4 },
|
||||
cardExcerpt: { fontSize: 13, color: Colors.gray[500], lineHeight: 18 },
|
||||
cardDate: { fontSize: 11, color: Colors.gray[400], marginTop: 4 },
|
||||
loading: { padding: 40, alignItems: 'center' },
|
||||
loadingText: { color: Colors.gray[400] },
|
||||
empty: { alignItems: 'center', paddingTop: 60 },
|
||||
emptyText: { fontSize: 16, color: Colors.gray[400], marginTop: 12 },
|
||||
cardTitle: { fontSize: FontSize.body, fontWeight: FontWeight.bold, color: Colors.black, marginBottom: Spacing.xs },
|
||||
cardExcerpt: { fontSize: FontSize.xs, color: Colors.gray[500], lineHeight: 18 },
|
||||
cardDate: { fontSize: FontSize.tiny, color: Colors.gray[400], marginTop: Spacing.xs },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user