Files
cesizen-individuel/mobile/app/(tabs)/journal/new.tsx
T
Azmog 7fd27d8dc2 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>
2026-06-19 15:22:15 +02:00

180 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
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();
const [step, setStep] = useState(1);
const [emotionId, setEmotionId] = useState<number | undefined>();
const [intensite, setIntensite] = useState(5);
const [note, setNote] = useState('');
const handleSubmit = async () => {
if (!emotionId) return;
try {
await createSaisie.mutateAsync({
emotion_id: emotionId,
intensite,
note: note.trim() || undefined,
date_saisie: new Date().toISOString().split('T')[0],
});
Alert.alert('Succès', 'Saisie enregistrée !', [{ text: 'OK', onPress: () => router.back() }]);
} catch {
Alert.alert('Erreur', "Impossible d'enregistrer la saisie");
}
};
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
{/* Stepper */}
<View style={styles.stepper}>
{[1, 2, 3].map((s) => (
<View key={s} style={styles.stepRow}>
<View style={[styles.stepCircle, step >= s && styles.stepActive]}>
{step > s ? (
<Ionicons name="checkmark" size={18} color={Colors.white} />
) : (
<Text style={[styles.stepNumber, step >= s && styles.stepNumberActive]}>{s}</Text>
)}
</View>
{s < 3 && <View style={[styles.stepLine, step > s && styles.stepLineActive]} />}
</View>
))}
</View>
{/* Step 1: Émotion */}
{step === 1 && (
<View>
<Text style={styles.stepTitle}>Comment vous sentez-vous ?</Text>
<EmotionPicker value={emotionId} onChange={setEmotionId} />
<View style={styles.navRowEnd}>
<Button
title="Suivant"
icon="arrow-forward"
onPress={() => emotionId && setStep(2)}
disabled={!emotionId}
/>
</View>
</View>
)}
{/* Step 2: Intensité */}
{step === 2 && (
<View>
<Text style={styles.stepTitle}>À quel point ressentez-vous cette émotion ?</Text>
<View style={styles.intensityContainer}>
<Text style={styles.intensityValue}>{intensite}</Text>
<Text style={styles.intensityLabel}>/10</Text>
</View>
<IntensityPicker value={intensite} onChange={setIntensite} />
<View style={styles.scaleLabels}>
<Text style={styles.scaleLabel}>Faible</Text>
<Text style={styles.scaleLabel}>Forte</Text>
</View>
<View style={styles.navRow}>
<Button title="Retour" variant="secondary" icon="arrow-back" onPress={() => setStep(1)} />
<Button title="Suivant" icon="arrow-forward" onPress={() => setStep(3)} />
</View>
</View>
)}
{/* Step 3: Note */}
{step === 3 && (
<View>
<Text style={styles.stepTitle}>Souhaitez-vous ajouter un commentaire ?</Text>
<TextField
label="Note (optionnel)"
value={note}
onChangeText={setNote}
placeholder="Décrivez ce que vous ressentez, ce qui a déclenché cette émotion…"
multiline
numberOfLines={5}
/>
<View style={styles.navRow}>
<Button title="Retour" variant="secondary" icon="arrow-back" onPress={() => setStep(2)} />
<Button
title="Enregistrer"
icon="checkmark"
loading={createSaisie.isPending}
onPress={handleSubmit}
/>
</View>
</View>
)}
</ScrollView>
);
}
/** Sélecteur d'intensité 1–10, boutons ≥40px, sur deux rangées (wrap). */
export function IntensityPicker({ value, onChange }: { value: number; onChange: (v: number) => void }) {
return (
<View style={pickerStyles.grid}>
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((v) => {
const active = value === v;
return (
<TouchableOpacity
key={v}
style={[pickerStyles.dot, active && pickerStyles.dotActive]}
onPress={() => onChange(v)}
accessibilityLabel={`Intensité ${v}`}
>
<Text style={[pickerStyles.dotText, active && pickerStyles.dotTextActive]}>{v}</Text>
</TouchableOpacity>
);
})}
</View>
);
}
const pickerStyles = StyleSheet.create({
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: Spacing.sm, justifyContent: 'center' },
dot: {
width: 44,
height: 44,
borderRadius: Radius.sm,
backgroundColor: Colors.gray[100],
borderWidth: 1,
borderColor: Colors.gray[200],
justifyContent: 'center',
alignItems: 'center',
},
dotActive: { backgroundColor: Colors.primary, borderColor: Colors.primary },
dotText: { fontSize: FontSize.sm, fontWeight: FontWeight.bold, color: Colors.gray[500] },
dotTextActive: { color: Colors.white },
});
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: Colors.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: 36,
height: 36,
borderRadius: Radius.pill,
backgroundColor: Colors.gray[200],
justifyContent: 'center',
alignItems: 'center',
},
stepActive: { backgroundColor: Colors.primary },
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: Spacing.xs },
stepLineActive: { backgroundColor: Colors.primary },
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 },
});