initial commit

This commit is contained in:
2026-09-04 20:05:29 +02:00
parent bc63380515
commit e4ed54a6e2
13 changed files with 9123 additions and 0 deletions
+203
View File
@@ -0,0 +1,203 @@
import React, { useCallback, useRef, useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
} from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
import { obtenirClasse, effectuerTirage, reTirer } from '../storage';
import { couleurs } from '../theme';
function formaterDate(iso) {
const d = new Date(iso);
const pad = (n) => String(n).padStart(2, '0');
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(
d.getHours()
)}:${pad(d.getMinutes())}`;
}
export default function DetailClasseScreen({ route, navigation }) {
const { classeId } = route.params;
const [classe, setClasse] = useState(null);
const [enCours, setEnCours] = useState(false);
// Ref en plus du state : le state ne se répercute qu'au prochain rendu,
// ce qui laisse une fenêtre où un second tap très rapide (avant que le
// bouton soit visuellement désactivé) pourrait lancer un 2e tirage pendant
// que le premier écrit encore dans le stockage. La ref, elle, est lue et
// mise à jour de façon synchrone, donc elle bloque le double-tap immédiatement.
const enCoursRef = useRef(false);
const recharger = useCallback(async () => {
const c = await obtenirClasse(classeId);
setClasse(c);
if (c) navigation.setOptions({ title: c.nom });
}, [classeId]);
useFocusEffect(
useCallback(() => {
recharger();
}, [recharger])
);
const tirer = async () => {
if (enCoursRef.current) return;
enCoursRef.current = true;
setEnCours(true);
try {
const c = await effectuerTirage(classeId);
setClasse(c);
} finally {
enCoursRef.current = false;
setEnCours(false);
}
};
const reTirerAction = async () => {
if (enCoursRef.current) return;
enCoursRef.current = true;
setEnCours(true);
try {
const c = await reTirer(classeId);
setClasse(c);
} finally {
enCoursRef.current = false;
setEnCours(false);
}
};
if (!classe) {
return (
<View style={[styles.container, styles.centre]}>
<ActivityIndicator />
</View>
);
}
const dernierTirage = classe.tirages[0];
return (
<View style={styles.container}>
<Text style={styles.sousTitre}>
{classe.nombreEleves} élèves · pondération max ×{classe.ponderationMax}
</Text>
<View style={styles.cardTirage}>
<TouchableOpacity
style={[styles.bouton, enCours && styles.boutonDesactive]}
onPress={tirer}
disabled={enCours}
>
<Text style={styles.boutonTexte}>🎲 Tirer un élève au sort</Text>
</TouchableOpacity>
{dernierTirage && (
<>
<Text style={styles.numeroTire}>n°{dernierTirage.numeroEleve}</Text>
<TouchableOpacity
style={[styles.boutonSecondaire, enCours && styles.boutonDesactive]}
onPress={reTirerAction}
disabled={enCours}
>
<Text style={styles.boutonSecondaireTexte}>
🔁 Re-tirer (annule et retire)
</Text>
</TouchableOpacity>
</>
)}
</View>
<Text style={styles.titreHistorique}>Historique des tirages</Text>
<FlatList
data={classe.tirages}
keyExtractor={(item) => item.id}
contentContainerStyle={{ paddingBottom: 24 }}
ListEmptyComponent={
<Text style={styles.texteVide}>Aucun tirage pour le moment.</Text>
}
renderItem={({ item, index }) => (
<View style={styles.ligneHistorique}>
<View style={styles.badge}>
<Text style={styles.badgeTexte}>{item.numeroEleve}</Text>
</View>
<Text style={styles.dateHistorique}>{formaterDate(item.dateTirage)}</Text>
{index === 0 && <Text style={styles.etiquetteRecent}>dernier</Text>}
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: couleurs.fond, padding: 16 },
centre: { justifyContent: 'center', alignItems: 'center' },
sousTitre: { color: couleurs.texteSecondaire, marginBottom: 14, fontSize: 14 },
cardTirage: {
backgroundColor: couleurs.carte,
borderRadius: 14,
padding: 20,
alignItems: 'center',
marginBottom: 22,
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 4,
shadowOffset: { width: 0, height: 1 },
elevation: 1,
},
bouton: {
backgroundColor: couleurs.primaire,
borderRadius: 10,
paddingVertical: 14,
paddingHorizontal: 20,
alignItems: 'center',
width: '100%',
},
boutonDesactive: { opacity: 0.6 },
boutonTexte: { color: 'white', fontSize: 16, fontWeight: '600' },
numeroTire: {
fontSize: 52,
fontWeight: '800',
color: couleurs.primaire,
marginVertical: 14,
},
boutonSecondaire: {
borderRadius: 10,
paddingVertical: 12,
paddingHorizontal: 20,
alignItems: 'center',
width: '100%',
backgroundColor: couleurs.fond,
borderWidth: 1,
borderColor: couleurs.bordure,
},
boutonSecondaireTexte: { color: couleurs.texte, fontSize: 15, fontWeight: '600' },
titreHistorique: { fontSize: 16, fontWeight: '700', marginBottom: 10, color: couleurs.texte },
texteVide: { color: couleurs.texteSecondaire },
ligneHistorique: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: couleurs.bordure,
},
badge: {
backgroundColor: couleurs.primaire,
borderRadius: 14,
width: 28,
height: 28,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
badgeTexte: { color: 'white', fontWeight: '700', fontSize: 13 },
dateHistorique: { color: couleurs.texteSecondaire, flex: 1 },
etiquetteRecent: {
fontSize: 11,
color: couleurs.succes,
fontWeight: '700',
textTransform: 'uppercase',
},
});