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:
@@ -4,6 +4,8 @@ import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useSaisies, useDeleteSaisie } from '@/hooks/useTracker';
|
||||
import { Colors } from '@/lib/colors';
|
||||
import { Spacing, Radius, FontSize, FontWeight, Shadow, MIN_TOUCH, HIT_SLOP } from '@/lib/theme';
|
||||
import { AppBar, Card, Badge, EmptyState, Loader } from '@/components/ui';
|
||||
import type { SaisieTracker } from '@/types';
|
||||
|
||||
export default function JournalScreen() {
|
||||
@@ -36,86 +38,128 @@ export default function JournalScreen() {
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: SaisieTracker }) => (
|
||||
<View style={styles.saisieCard}>
|
||||
<Card padding={Spacing.lg} style={styles.saisieCard}>
|
||||
<View style={[styles.emotionIcon, { backgroundColor: `${item.emotion?.couleur}20` }]}>
|
||||
<Text style={styles.emotionEmoji}>{item.emotion?.icone || '🔵'}</Text>
|
||||
</View>
|
||||
<View style={styles.saisieContent}>
|
||||
<View style={styles.saisieHeader}>
|
||||
<View style={[styles.badge, { backgroundColor: `${item.emotion?.couleur}20` }]}>
|
||||
<Text style={[styles.badgeText, { color: item.emotion?.couleur }]}>{item.emotion?.nom}</Text>
|
||||
</View>
|
||||
<Text style={styles.intensityText}>Intensité: {item.intensite}/10</Text>
|
||||
{item.emotion?.couleur ? (
|
||||
<Badge label={item.emotion?.nom || ''} color={item.emotion.couleur} />
|
||||
) : (
|
||||
<Badge label={item.emotion?.nom || '—'} tone="neutral" />
|
||||
)}
|
||||
<Text style={styles.intensityText}>Intensité {item.intensite}/10</Text>
|
||||
</View>
|
||||
<View style={styles.intensityBar}>
|
||||
<View style={[styles.intensityFill, { width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary }]} />
|
||||
<View
|
||||
style={[
|
||||
styles.intensityFill,
|
||||
{ width: `${item.intensite * 10}%`, backgroundColor: item.emotion?.couleur || Colors.primary },
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
{item.note && <Text style={styles.note} numberOfLines={2}>{item.note}</Text>}
|
||||
<Text style={styles.date}>{new Date(item.date_saisie).toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}</Text>
|
||||
{item.note ? (
|
||||
<Text style={styles.note} numberOfLines={2}>
|
||||
{item.note}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.date}>
|
||||
{new Date(item.date_saisie).toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}>
|
||||
<Ionicons name="create-outline" size={18} color={Colors.gray[400]} />
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
hitSlop={HIT_SLOP}
|
||||
accessibilityLabel="Modifier la saisie"
|
||||
onPress={() => router.push({ pathname: '/(tabs)/journal/edit', params: { id: String(item.id) } })}
|
||||
>
|
||||
<Ionicons name="create-outline" size={20} color={Colors.primary} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={() => handleDelete(item.id)}>
|
||||
<Ionicons name="trash-outline" size={18} color={Colors.gray[400]} />
|
||||
<TouchableOpacity
|
||||
style={styles.actionBtn}
|
||||
hitSlop={HIT_SLOP}
|
||||
accessibilityLabel="Supprimer la saisie"
|
||||
onPress={() => handleDelete(item.id)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color={Colors.error} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={saisies || []}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary} />}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="book-outline" size={48} color={Colors.gray[300]} />
|
||||
<Text style={styles.emptyTitle}>Aucune saisie</Text>
|
||||
<Text style={styles.emptyText}>Commencez par enregistrer votre première émotion</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
<TouchableOpacity style={styles.fab} onPress={() => router.push('/(tabs)/journal/new')}>
|
||||
<Ionicons name="add" size={28} color={Colors.white} />
|
||||
<View style={styles.screen}>
|
||||
<AppBar title="Mon journal" subtitle="Vos émotions au fil du temps" />
|
||||
{isLoading ? (
|
||||
<Loader label="Chargement du journal…" />
|
||||
) : (
|
||||
<FlatList
|
||||
data={saisies || []}
|
||||
keyExtractor={(item) => String(item.id)}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={Colors.primary} />
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
icon="book-outline"
|
||||
title="Aucune saisie"
|
||||
message="Commencez par enregistrer votre première émotion."
|
||||
actionLabel="Nouvelle saisie"
|
||||
onAction={() => router.push('/(tabs)/journal/new')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
accessibilityLabel="Nouvelle saisie"
|
||||
onPress={() => router.push('/(tabs)/journal/new')}
|
||||
>
|
||||
<Ionicons name="add" size={30} color={Colors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: Colors.white },
|
||||
listContent: { padding: 16, paddingBottom: 80 },
|
||||
saisieCard: {
|
||||
flexDirection: 'row', alignItems: 'flex-start', gap: 12, padding: 16,
|
||||
backgroundColor: Colors.white, borderRadius: 4, marginBottom: 10,
|
||||
borderWidth: 1, borderColor: Colors.gray[200],
|
||||
},
|
||||
emotionIcon: { width: 48, height: 48, borderRadius: 4, justifyContent: 'center', alignItems: 'center' },
|
||||
screen: { flex: 1, backgroundColor: Colors.gray[50] },
|
||||
listContent: { padding: Spacing.lg, paddingBottom: 96 },
|
||||
saisieCard: { flexDirection: 'row', alignItems: 'flex-start', gap: Spacing.md, marginBottom: Spacing.md },
|
||||
emotionIcon: { width: 48, height: 48, borderRadius: Radius.sm, justifyContent: 'center', alignItems: 'center' },
|
||||
emotionEmoji: { fontSize: 24 },
|
||||
saisieContent: { flex: 1 },
|
||||
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 6 },
|
||||
badge: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 4 },
|
||||
badgeText: { fontSize: 13, fontWeight: '600' },
|
||||
intensityText: { fontSize: 12, color: Colors.gray[400] },
|
||||
intensityBar: { height: 4, backgroundColor: Colors.gray[200], borderRadius: 2, marginBottom: 6 },
|
||||
intensityFill: { height: 4, borderRadius: 2 },
|
||||
note: { fontSize: 13, color: Colors.gray[600], marginBottom: 4 },
|
||||
date: { fontSize: 12, color: Colors.gray[400] },
|
||||
actions: { gap: 4 },
|
||||
actionBtn: { padding: 6, borderRadius: 8 },
|
||||
empty: { alignItems: 'center', paddingTop: 80 },
|
||||
emptyTitle: { fontSize: 18, fontWeight: '600', color: Colors.gray[400], marginTop: 12 },
|
||||
emptyText: { fontSize: 14, color: Colors.gray[400], marginTop: 4 },
|
||||
saisieHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, marginBottom: Spacing.sm, flexWrap: 'wrap' },
|
||||
intensityText: { fontSize: FontSize.xs, color: Colors.gray[500] },
|
||||
intensityBar: { height: 6, backgroundColor: Colors.gray[200], borderRadius: Radius.sm, marginBottom: Spacing.sm },
|
||||
intensityFill: { height: 6, borderRadius: Radius.sm },
|
||||
note: { fontSize: FontSize.xs, color: Colors.gray[600], marginBottom: Spacing.xs, lineHeight: 19 },
|
||||
date: { fontSize: FontSize.xs, color: Colors.gray[400], textTransform: 'capitalize' },
|
||||
actions: { gap: Spacing.sm },
|
||||
actionBtn: {
|
||||
width: MIN_TOUCH,
|
||||
height: MIN_TOUCH,
|
||||
borderRadius: Radius.sm,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute', right: 20, bottom: 20,
|
||||
width: 56, height: 56, borderRadius: 4,
|
||||
backgroundColor: Colors.primary, justifyContent: 'center', alignItems: 'center',
|
||||
shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8,
|
||||
elevation: 6,
|
||||
position: 'absolute',
|
||||
right: Spacing.lg,
|
||||
bottom: Spacing.lg,
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: Radius.sm,
|
||||
backgroundColor: Colors.primary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...Shadow.raised,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user